diff --git a/Saved/Songs/Example/Config.json b/Saved/Songs/Example/Config.json deleted file mode 100644 index e4b457e..0000000 --- a/Saved/Songs/Example/Config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Name": "Dubidubidu", - "Year": 2003, - "Artist": "Christell", - "Length": 224, - "UUID": "runit", - "PreviewTime": 0, - "MinorMajor": "Minor", - "Key": "A", - "Album": "Christell", - "GuitarType": "Keytar", - "BeatsPerMinute": 145, - "AssetID": "SID_Placeholder_59", - "JoinCode": "0000-0000-0000", - "LipsyncData": "https://cdn2.unrealengine.com/saveyourtears-3b556f295b86.lad", - "Cover": "https://steamuserimages-a.akamaihd.net/ugc/2319980071298431643/13F80F955F3612EF407370E522ABA956D690374E/?imw=512&&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false" -} \ No newline at end of file diff --git a/Saved/Songs/Example/Dogsong.mp3 b/Saved/Songs/Example/Dogsong.mp3 deleted file mode 100644 index c0197b5..0000000 Binary files a/Saved/Songs/Example/Dogsong.mp3 and /dev/null differ diff --git a/Server/Saved/Songs/ButterBarnHoedown/Cover.png b/Server/Saved/Songs/ButterBarnHoedown/Cover.png new file mode 100644 index 0000000..99331c5 Binary files /dev/null and b/Server/Saved/Songs/ButterBarnHoedown/Cover.png differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00001.m4s b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00001.m4s new file mode 100644 index 0000000..eb617fd Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00001.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00002.m4s b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00002.m4s new file mode 100644 index 0000000..9908a37 Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00002.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00003.m4s b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00003.m4s new file mode 100644 index 0000000..653867c Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00003.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00004.m4s b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00004.m4s new file mode 100644 index 0000000..2dc6e1a Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00004.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00005.m4s b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00005.m4s new file mode 100644 index 0000000..c0c4bde Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00005.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00006.m4s b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00006.m4s new file mode 100644 index 0000000..f34178d Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00006.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00007.m4s b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00007.m4s new file mode 100644 index 0000000..d2af797 Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00007.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00008.m4s b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00008.m4s new file mode 100644 index 0000000..62bd3e0 Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00008.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Chunks/init-stream0.m4s b/Server/Saved/Songs/Dogsong/Chunks/init-stream0.m4s new file mode 100644 index 0000000..dcd866d Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Chunks/init-stream0.m4s differ diff --git a/Server/Saved/Songs/Dogsong/Cover.png b/Server/Saved/Songs/Dogsong/Cover.png new file mode 100644 index 0000000..dd660ca Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Cover.png differ diff --git a/Server/Saved/Songs/Dogsong/Data.mid b/Server/Saved/Songs/Dogsong/Data.mid new file mode 100644 index 0000000..50e8d7b Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Data.mid differ diff --git a/Server/Saved/Songs/Dogsong/Dogsong.m4a b/Server/Saved/Songs/Dogsong/Dogsong.m4a new file mode 100644 index 0000000..1962042 Binary files /dev/null and b/Server/Saved/Songs/Dogsong/Dogsong.m4a differ diff --git a/Server/Saved/Songs/Dogsong/Manifest.mpd b/Server/Saved/Songs/Dogsong/Manifest.mpd new file mode 100644 index 0000000..2267abf --- /dev/null +++ b/Server/Saved/Songs/Dogsong/Manifest.mpd @@ -0,0 +1,31 @@ + + + {BASEURL} + + Dogsong + + + + + + + + + + + + + + + + + + diff --git a/Server/Saved/Songs/Zombies/Audio.m4a b/Server/Saved/Songs/Zombies/Audio.m4a new file mode 100644 index 0000000..85b4412 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Audio.m4a differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00001.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00001.m4s new file mode 100644 index 0000000..06ff27c Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00001.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00002.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00002.m4s new file mode 100644 index 0000000..76ef949 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00002.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00003.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00003.m4s new file mode 100644 index 0000000..797f479 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00003.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00004.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00004.m4s new file mode 100644 index 0000000..a9d8aa8 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00004.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00005.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00005.m4s new file mode 100644 index 0000000..901a925 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00005.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00006.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00006.m4s new file mode 100644 index 0000000..c75ede1 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00006.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00007.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00007.m4s new file mode 100644 index 0000000..d9ad70f Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00007.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00008.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00008.m4s new file mode 100644 index 0000000..a91ee19 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00008.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00009.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00009.m4s new file mode 100644 index 0000000..7b17b47 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00009.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00010.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00010.m4s new file mode 100644 index 0000000..d2c0cbe Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00010.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00011.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00011.m4s new file mode 100644 index 0000000..56494d7 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00011.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00012.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00012.m4s new file mode 100644 index 0000000..f59f35d Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00012.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00013.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00013.m4s new file mode 100644 index 0000000..3803b5e Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00013.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00014.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00014.m4s new file mode 100644 index 0000000..6c24bfe Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00014.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00015.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00015.m4s new file mode 100644 index 0000000..1e07b4c Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00015.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00016.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00016.m4s new file mode 100644 index 0000000..734a8b5 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00016.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00017.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00017.m4s new file mode 100644 index 0000000..fffcb20 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00017.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00018.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00018.m4s new file mode 100644 index 0000000..1e7d7b3 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00018.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00019.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00019.m4s new file mode 100644 index 0000000..f3f592a Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00019.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00020.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00020.m4s new file mode 100644 index 0000000..e940d84 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00020.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00021.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00021.m4s new file mode 100644 index 0000000..402242f Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00021.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00022.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00022.m4s new file mode 100644 index 0000000..a51f5cd Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00022.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00023.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00023.m4s new file mode 100644 index 0000000..3de7f6d Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00023.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00024.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00024.m4s new file mode 100644 index 0000000..1302152 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00024.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00025.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00025.m4s new file mode 100644 index 0000000..53e30ce Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00025.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00026.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00026.m4s new file mode 100644 index 0000000..88d7218 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00026.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00027.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00027.m4s new file mode 100644 index 0000000..115a102 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00027.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00028.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00028.m4s new file mode 100644 index 0000000..9f98ebb Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00028.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00029.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00029.m4s new file mode 100644 index 0000000..cfc6a5b Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00029.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00030.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00030.m4s new file mode 100644 index 0000000..989e109 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00030.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00031.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00031.m4s new file mode 100644 index 0000000..fe8de88 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00031.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00032.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00032.m4s new file mode 100644 index 0000000..9614ba6 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00032.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00033.m4s b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00033.m4s new file mode 100644 index 0000000..6334bae Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00033.m4s differ diff --git a/Server/Saved/Songs/Zombies/Chunks/init-stream0.m4s b/Server/Saved/Songs/Zombies/Chunks/init-stream0.m4s new file mode 100644 index 0000000..dcd866d Binary files /dev/null and b/Server/Saved/Songs/Zombies/Chunks/init-stream0.m4s differ diff --git a/Server/Saved/Songs/Zombies/Cover.png b/Server/Saved/Songs/Zombies/Cover.png new file mode 100644 index 0000000..ffb6a32 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Cover.png differ diff --git a/Server/Saved/Songs/Zombies/Data.mid b/Server/Saved/Songs/Zombies/Data.mid new file mode 100644 index 0000000..d1f1753 Binary files /dev/null and b/Server/Saved/Songs/Zombies/Data.mid differ diff --git a/Server/Saved/Songs/Zombies/Manifest.mpd b/Server/Saved/Songs/Zombies/Manifest.mpd new file mode 100644 index 0000000..ede6cb9 --- /dev/null +++ b/Server/Saved/Songs/Zombies/Manifest.mpd @@ -0,0 +1,30 @@ + + + {BASEURL} + + + + + + + + + + + + + + + + + + + diff --git a/Server/Source/Handlers/Server.ts b/Server/Source/Handlers/Server.ts index e94658c..044f208 100644 --- a/Server/Source/Handlers/Server.ts +++ b/Server/Source/Handlers/Server.ts @@ -1,7 +1,7 @@ import e from "express" import fs from "fs"; import { BODY_SIZE_LIMIT, COOKIE_SIGN_KEY, DASHBOARD_ROOT, ENDPOINT_AUTHENTICATION_ENABLED, ENDPOINT_AUTH_HEADER, ENDPOINT_AUTH_VALUE, IS_DEBUG, PORT, PROJECT_NAME, SERVER_URL } from "../Modules/Constants"; -import { Msg, Warn } from "../Modules/Logger"; +import { Debug, Msg, Warn } from "../Modules/Logger"; import { italic, magenta, red, yellow } from "colorette"; import cookieParser from "cookie-parser"; import path from "path"; @@ -30,6 +30,7 @@ async function Initialize() { if (ENDPOINT_AUTHENTICATION_ENABLED && req.header(ENDPOINT_AUTH_HEADER as string) !== ENDPOINT_AUTH_VALUE) return res.status(403).send(`${SERVER_URL} is currently locked behind authentication. Come back later!`); + Debug(req.path); next(); }) @@ -41,6 +42,8 @@ async function Initialize() { Msg(`Loaded route ${italic(File)}!`); } + + App.use((_, res) => res.status(404).json({ errorMessage: "Not Found" })); App.listen(PORT, () => Msg(`${magenta(PROJECT_NAME)} now up on port ${magenta(PORT)} ${(IS_DEBUG ? red("(debug environment)") : "")}`)); } @@ -48,11 +51,7 @@ async function Initialize() { Initialize(); // ! FESTIVAL-SPECIFIC STUFF -// set up both utils for usage -import { LoadSongs } from "../Modules/FestivalUtil"; -import { CacheFortnitePages } from "../Modules/PagesUtil"; import axios from "axios"; axios.defaults.validateStatus = () => true; -LoadSongs(); -CacheFortnitePages(); \ No newline at end of file +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 62659b2..5198f16 100644 --- a/Server/Source/Modules/FNUtil.ts +++ b/Server/Source/Modules/FNUtil.ts @@ -2,31 +2,64 @@ import axios from "axios"; import { Err } from "./Logger"; import { red } from "colorette"; import { FULL_SERVER_ROOT } from "./Constants"; -import { AvailableFestivalSongs } from "./FestivalUtil"; +import { User } from "../Schemas/User"; +import { Song } from "../Schemas/Song"; -export let FullFortnitePages: object | null = null; +export let FullFortnitePages: {[key: string]: any} | null = null; +export let OriginalSparks: {[key: string]: any} | null = null; let LastContentDownloadDate: Date = new Date(0); // set it to 1970 as default cuz im not boutta check if its null -export async function GenerateFortnitePages(): Promise<{ Success: boolean, FNPages: { [key: string]: unknown } | null }> { +GenerateFortnitePages(null); + +export async function GenerateFortnitePages(ForUser: User | null): Promise<{ Success: boolean, FNPages: { [key: string]: unknown } | null }> { 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 axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game") : { status: 200, data: FullFortnitePages }; - FullFortnitePages = data; + 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, + sparkTracks: { + ...data.sparkTracks, + lastModified: new Date().toISOString() + } + }; + OriginalSparks = OGSparks.data; LastContentDownloadDate = new Date(); - if (status !== 200) { - Err(`Failed to get Fortnite pages: ${red(status)}`); + if (!ForUser) + return { Success: true, FNPages: null }; + + if (status !== 200 || OGSparks.status !== 200) { + Err(`Failed to get Fortnite pages: ${red(status)}, ${red(OGSparks.status)}`); console.log(data); process.exit(-1); // very big fuck moment, we literally cannot run the server without fortnitepages } const AllSongs: { [key: string]: unknown } = {}; // too lazy to actually write a schema for this :D - for (const Song of AvailableFestivalSongs) + const Overrides = ForUser.Library.map(x => { return { ...x, SongData: Song.findOne({ where: { ID: x.SongID } }) }; }); + const UsersLibrary = await Promise.all(Overrides.map(x => x.SongData)); + + for (const Song of UsersLibrary) { - AllSongs[Song.UUID] = { - _title: Song.UUID, + if (!Song) + continue; + + const OverridingAs = Overrides.find(x => x.SongID === Song.ID); + if (!OverridingAs) + continue; + + const OriginalTrack = Object.values(OriginalSparks!).find(x => x.track?.ti === `SparksSong:${OverridingAs.Overriding.toLowerCase()}`); + if (!OriginalTrack) + continue; + + AllSongs[OriginalTrack._title] = { + _title: OriginalTrack._title, _noIndex: false, _activeDate: "2023-01-01T01:00:00.000Z", _locale: "en-US", @@ -34,37 +67,37 @@ export async function GenerateFortnitePages(): Promise<{ Success: boolean, FNPag lastModified: new Date().toISOString(), track: { tt: Song.Name, // tt - Title, - an: Song.Artist, // an - Artist Name - mm: Song.MinorMajor, // mm - Minor, Major + an: Song.ArtistName, // an - Artist Name + mm: Song.Scale, // mm - Minor, Major mk: Song.Key, // mk - Music Key ab: Song.Album, // ab - Album - su: Song.UUID, // su - Song UUID + su: OriginalTrack._title, // su - Song UUID ry: Song.Year, // ry - Release Year - mt: Song.BeatsPerMinute, // mt - Music Timing (?) - au: Song.Cover ?? `${FULL_SERVER_ROOT}/song/download/${Song.UUID}/cover.png`, // au - Album Cover + mt: Song.Tempo, // mt - Music Timing (?) + au: Song.Cover ?? `${FULL_SERVER_ROOT}/song/download/${Song.ID}/cover.png`, // au - Album Cover gt: [ "Jam-LoopIsUnpitched-Beat" ], // gt - Gameplay Tags (but in a different format: Example.Gameplay.Tag -> Example-Gameplay-Tag) - ti: `SparksSong:${Song.AssetID.toLowerCase()}`, - mu: Song.Midi ?? `${FULL_SERVER_ROOT}/song/download/${Song.UUID}/midi.mid`, // mu - Song Midi (if ending with .mid, decrypted, if with .dat, encrypted) + ti: `SparksSong:${OverridingAs.Overriding.toLowerCase()}`, + mu: Song.Midi ?? `${FULL_SERVER_ROOT}/song/download/${Song.ID}/midi.mid`, // mu - Song Midi (encrypted) dn: Song.Length, // dn - Track Length (in seconds) ge: [ "Pop" ], // ge - Genres - in: { // TODO: fuck with this to make difficulties :+1: - ba: 1, - pb: 2, - pd: 3, - pg: 4, - vl: 3, - ds: 2, - gr: 1, + in: { + ba: Song.BassDifficulty, + pb: Song.BassDifficulty, + pd: Song.DrumsDifficulty, + ds: Song.DrumsDifficulty, + pg: Song.GuitarDifficulty, + gr: Song.GuitarDifficulty, + vl: Song.VocalsDifficulty, _type: "SparkTrackIntensities" }, // in - Intensities (those white bars you see) sib: "Bass", // sib - Bass ID to use (only Bass possible) sid: "Drum", // sid - Drums ID to use (only Drum possible) - sig: Song.GuitarType, // sig - Guitar ID to use (Keytar/Guitar) + sig: Song.GuitarStarterType, // sig - Guitar ID to use (Keytar/Guitar) 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.UUID, // sid - Song UUID - pid: Song.UUID, // pid - Playlist Asset ID - title: Song.UUID, // title - Song Name - same as _title + sid: Song.ID, // sid - Song UUID + pid: Song.ID, // pid - Playlist Asset ID + title: OriginalTrack._title, // title - Song Name - same as _title tracks: [ { part: "ds", // Drum Set @@ -87,18 +120,18 @@ export async function GenerateFortnitePages(): Promise<{ Success: boolean, FNPag vols: [ 4, 4 ] }, { - part: "fs", // Fart Set (jk i have no idea) (might be Flare Set??????????????) + part: "fs", // Fart Set (jk i have no idea) channels: [ "FL", "FR" ], vols: [ 4, 4 ] } ], preview: { - starttime: Song.PreviewTime + starttime: 0 } }), - ld: Song.LipsyncData, // ld - Lipsync Data (it's literally a uasset) - jc: Song.JoinCode, // jc - Join Code (UEFN empty island with nothing - possibly downloads assets) - sn: Song.UUID, // sn - Song Name - same as _title + ld: Song.Lipsync ?? OriginalTrack.track.ld, // ld - Lipsync Data (it's literally a uasset) + jc: OriginalTrack.track.jc, // jc - Join Code (UEFN empty island with nothing - possibly downloads assets) + sn: OriginalTrack._title, // sn - Song Name - same as _title _type: "SparkTrack" } }; diff --git a/Server/Source/Modules/FestivalUtil.ts b/Server/Source/Modules/FestivalUtil.ts deleted file mode 100644 index 89426dc..0000000 --- a/Server/Source/Modules/FestivalUtil.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { existsSync, lstatSync, readFileSync, readdirSync } from "fs"; -import { SongItemDefinition } from "./Classes"; -import { CacheFortnitePages } from "./PagesUtil"; -import { Debug, Err, Warn } from "./Logger"; -import { magenta, yellow } from "colorette"; -import watch from "node-watch"; - -export let AvailableFestivalSongs: SongItemDefinition[] = []; - -export function LoadSongs() { - AvailableFestivalSongs = - readdirSync("../Saved/Songs") - .filter(f => lstatSync(`../Saved/Songs/${f}`).isDirectory() && existsSync(`../Saved/Songs/${f}/Config.json`)) - .map(f => { - let Config: SongItemDefinition; - - try { Config = JSON.parse(readFileSync(`../Saved/Songs/${f}/Config.json`).toString()); } - catch { Err(`Config for song ${f} failed to parse. Please make sure it's valid!`); process.exit(-1); } - - // todo: validate if it has all the required properities - Debug(`Added ${magenta(`${Config.Artist} - ${Config.Name} (${Config.UUID})`)} to the list of available Festival songs!`); - - return { - Directory: `../Saved/Songs/${f}`, - ...Config - }; - }); -} - -watch("../Saved/Songs", { recursive: true }, (Event, Filename) => { - Warn(`Detected ${yellow("saved songs")} changes in ${yellow(Filename)}. Reloading available songs!`); - Debug(`${magenta(Event)} on ${magenta(Filename)}`); - LoadSongs(); - CacheFortnitePages(); -}) \ No newline at end of file diff --git a/Server/Source/Modules/Middleware.ts b/Server/Source/Modules/Middleware.ts new file mode 100644 index 0000000..aceb607 --- /dev/null +++ b/Server/Source/Modules/Middleware.ts @@ -0,0 +1,35 @@ +import { NextFunction, Request, Response } from "express"; +import { User } from "../Schemas/User"; +import { JwtPayload, verify } from "jsonwebtoken"; +import { IS_DEBUG, JWT_KEY } from "./Constants"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + interface Request { + user?: User; + } + } +} + +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).json({ errorMessage: "This endpoint requires authorization." }); + + let JWT: JwtPayload; + try { + JWT = verify(req.header("X-Partypack-Token") ?? req.cookies["Token"] ?? req.header("Authorization"), JWT_KEY!) as JwtPayload; + } catch (err) { + console.error(err); + return res.status(403).json({ errorMessage: `Invalid Partypack token provided.${IS_DEBUG ? ` (${err})` : ""}` }); + } + + const UserData = await User.findOne({ where: { ID: JWT.ID }, relations: Relations }); + if (!UserData) + return res.status(401).json({ errorMessage: "Invalid Partypack token provided. User does not exist in database. Please contact an instance admin." }); + + req.user = UserData; + next(); + } +} \ No newline at end of file diff --git a/Server/Source/Modules/PagesUtil.ts b/Server/Source/Modules/PagesUtil.ts deleted file mode 100644 index 1cd793c..0000000 --- a/Server/Source/Modules/PagesUtil.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GenerateFortnitePages } from "../Modules/FNUtil"; -import { Msg } from "../Modules/Logger"; - -export let CachedSongFortnitePages: { [key: string]: unknown } | null = null; - -export async function CacheFortnitePages() { - const Result = await GenerateFortnitePages(); - if (!Result.Success) - return; // fuck we're fucked - - CachedSongFortnitePages = Result.FNPages; - Msg("Successfully reloaded Fortnite pages!"); -} \ No newline at end of file diff --git a/Server/Source/Routes/Admin.ts b/Server/Source/Routes/Admin.ts index 4e06364..d2c691a 100644 --- a/Server/Source/Routes/Admin.ts +++ b/Server/Source/Routes/Admin.ts @@ -1,5 +1,6 @@ import { Router } from "express"; import { ADMIN_KEY } from "../Modules/Constants"; +import { Song } from "../Schemas/Song"; const App = Router(); @@ -11,7 +12,7 @@ App.use((req, res, next) => { if (req.path === "/key") return res.status(req.body.Key === ADMIN_KEY ? 200 : 403).send(req.body.Key === ADMIN_KEY ? "Login successful!" : "Key doesn't match. Try again."); - if (req.cookies["AdminKey"] !== ADMIN_KEY) + if ((req.cookies["AdminKey"] ?? req.header("Authorization")) !== ADMIN_KEY) return res.status(403).send("You don't have permission to access this endpoint."); next(); @@ -19,6 +20,10 @@ App.use((req, res, next) => { App.get("/test", (_, res) => res.send("Permission check OK")); +App.get("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package()))); + +App.post("/create/song", async (req, res) => res.json(await Song.create(req.body).save())); + export default { App, DefaultAPI: "/admin/api" diff --git a/Server/Source/Routes/Authentication.ts b/Server/Source/Routes/Authentication.ts index 54fda10..5db90ae 100644 --- a/Server/Source/Routes/Authentication.ts +++ b/Server/Source/Routes/Authentication.ts @@ -3,6 +3,7 @@ import jwt from "jsonwebtoken"; import qs from "querystring"; import { Response, Router } from "express"; import { DASHBOARD_ROOT, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, FULL_SERVER_ROOT, JWT_KEY } from "../Modules/Constants"; +import { User } from "../Schemas/User"; const App = Router(); @@ -35,6 +36,12 @@ App.get("/discord", async (req, res) => { await QuickRevokeToken(res, Discord.data.access_token); + if (!await User.exists({ where: { ID: UserData.data.id } })) + await User.create({ + ID: UserData.data.id, + Library: [] + }).save(); + res .cookie("Token", jwt.sign({ ID: UserData.data.id }, JWT_KEY!, { algorithm: "HS256" })) .cookie("UserDetails", Buffer.from(JSON.stringify({ ID: UserData.data.id, Username: UserData.data.username, GlobalName: UserData.data.global_name, Avatar: `https://cdn.discordapp.com/avatars/${UserData.data.id}/${UserData.data.avatar}.webp` })).toString("base64")) diff --git a/Server/Source/Routes/Discovery.ts b/Server/Source/Routes/Discovery.ts new file mode 100644 index 0000000..6c2733b --- /dev/null +++ b/Server/Source/Routes/Discovery.ts @@ -0,0 +1,24 @@ +import { Router } from "express"; +import { ForcedCategory } from "../Schemas/ForcedCategory"; +import { Song } from "../Schemas/Song"; + +const App = Router(); + +App.get("/", async (req, res) => { + const ForcedCategories = await ForcedCategory.find({ where: { Activated: true } }); + const New = { + ID: "new", + Header: "Recently added", + Songs: (await Song.find({ take: 10, order: { CreationDate: "DESC" } })).map(x => x.Package()) + } + + res.json([ + ...ForcedCategories, + New + ]) +}); + +export default { + App, + DefaultAPI: "/api/discovery" +} \ No newline at end of file diff --git a/Server/Source/Routes/Downloads.ts b/Server/Source/Routes/Downloads.ts index bbc400a..8835455 100644 --- a/Server/Source/Routes/Downloads.ts +++ b/Server/Source/Routes/Downloads.ts @@ -1,17 +1,18 @@ import { Router } from "express"; import { existsSync, readFileSync } from "fs"; -import { AvailableFestivalSongs } from "../Modules/FestivalUtil"; import { FULL_SERVER_ROOT } from "../Modules/Constants"; import { CreateBlurl } from "../Modules/BLURL"; +import { Song } from "../Schemas/Song"; const App = Router(); -App.get("/song/download/:SongUUID/:File", (req, res) => { - const Song = AvailableFestivalSongs.find(x => x.UUID === req.params.SongUUID); - if (!Song) - return res.sendStatus(404); +App.get("/song/download/:InternalID/:File", async (req, res) => { + //const Song = AvailableFestivalSongs.find(x => x.UUID === req.params.SongUUID); + const SongData = await Song.findOne({ where: { ID: req.params.InternalID } }); + if (!SongData) + return res.status(404).json({ errorMessage: "Song not found." }); - const BaseURL = `${FULL_SERVER_ROOT}/song/download/${Song.UUID}/`; + const BaseURL = `${FULL_SERVER_ROOT}/song/download/${SongData.ID}/`; switch (req.params.File.toLowerCase()) { case "master.blurl": case "main.blurl": @@ -22,7 +23,7 @@ App.get("/song/download/:SongUUID/:File", (req, res) => { type: "main", language: "en", url: `${BaseURL}master.blurl`, - data: readFileSync(`${Song.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL) + data: readFileSync(`${SongData.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL) } ], type: "vod", @@ -32,11 +33,11 @@ App.get("/song/download/:SongUUID/:File", (req, res) => { case "manifest": case "manifest.mpd": - return res.set("content-type", "application/dash+xml").send(Buffer.from(readFileSync(`${Song.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL))); + return res.set("content-type", "application/dash+xml").send(Buffer.from(readFileSync(`${SongData.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL))); case "cover": case "cover.png": - return existsSync(`${Song.Directory}/Cover.png`) ? res.set("content-type", "image/png").send(readFileSync(`${Song.Directory}/Cover.png`)) : res.sendStatus(404); + return existsSync(`${SongData.Directory}/Cover.png`) ? res.set("content-type", "image/png").send(readFileSync(`${SongData.Directory}/Cover.png`)) : res.sendStatus(404); // ! we are not risking a lawsuit //case "midi.dat": // dont forget to encrypt! @@ -46,7 +47,7 @@ App.get("/song/download/:SongUUID/:File", (req, res) => { case "midi": case "midi.mid": case "midi.midi": // forget to encrypt! - return existsSync(`${Song.Directory}/Data.mid`) ? res.set("content-type", "application/octet-stream").send(readFileSync(`${Song.Directory}/Data.mid`)) : res.sendStatus(404); + return existsSync(`${SongData.Directory}/Data.mid`) ? res.set("content-type", "application/octet-stream").send(readFileSync(`${SongData.Directory}/Data.mid`)) : res.sendStatus(404); } if (!/^[\w\-.]+$/g.test(req.params.File)) @@ -55,12 +56,32 @@ App.get("/song/download/:SongUUID/:File", (req, res) => { if (!req.params.File.endsWith(".m4s")) return res.sendStatus(403); - if (!existsSync(`${Song.Directory}/Chunks/${req.params.File}`)) + if (!existsSync(`${SongData.Directory}/Chunks/${req.params.File}`)) return res.sendStatus(404); res.set("content-type", "video/mp4") - res.send(readFileSync(`${Song.Directory}/Chunks/${req.params.File}`)); -}) + res.send(readFileSync(`${SongData.Directory}/Chunks/${req.params.File}`)); +}); + +App.get("/:InternalID", async (req, res) => { + const SongData = await Song.findOne({ where: { ID: req.params.InternalID } }); + if (!SongData) + return res.status(404).json({ errorMessage: "Song not found." }); + + const BaseURL = `${FULL_SERVER_ROOT}/song/download/${SongData.ID}/`; + res.set("content-type", "application/json"); + res.json({ + playlist: Buffer.from(readFileSync(`${SongData.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL)).toString("base64"), + playlistType: "application/dash+xml", + metadata: { + assetId: "", + baseUrls: [ BaseURL ], + supportsCaching: true, + ucp: "a", + version: Math.floor(Date.now() / 1000) + } + }); +}); export default { App diff --git a/Server/Source/Routes/Library.ts b/Server/Source/Routes/Library.ts index 1004a83..35d3279 100644 --- a/Server/Source/Routes/Library.ts +++ b/Server/Source/Routes/Library.ts @@ -1,13 +1,91 @@ import { Router } from "express"; +import { RequireAuthentication } from "../Modules/Middleware"; +import { Song } from "../Schemas/Song"; +import { OriginalSparks } from "../Modules/FNUtil"; const App = Router(); -App.get("/song/data/:InternalID", (req, res) => { +App.get("/me", RequireAuthentication({ BookmarkedSongs: true }), (req, res) => { res.json({ - + Bookmarks: req.user?.BookmarkedSongs.map(x => x.Package()), + Library: req.user?.Library }) }) +App.post("/me/activate", RequireAuthentication(), async (req, res) => { + if (!req.body.SongID || !req.body.ToOverride) + return res.status(400).json({ errorMessage: "You didn't provide a Song ID." }); + + if (!/^sid_placeholder_(\d){1,3}$/gi.test(req.body.ToOverride)) + return res.status(400).json({ errorMessage: "Field \"ToOverride\" must match \"sid_placeholder_\"" }); + + 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." }); + + if (!await Song.exists({ where: { ID: req.body.SongID } })) + return res.status(404).json({ errorMessage: "Provided song doesn't exist." }); + + req.user?.Library.push({ SongID: req.body.SongID.toLowerCase(), Overriding: req.body.ToOverride.toLowerCase() }); + req.user?.save(); + + res.json(req.user?.Library); +}) + +App.post("/me/deactivate", RequireAuthentication(), async (req, res) => { + if (!req.body.SongID) + return res.status(400).json({ errorMessage: "You didn't provide a Song ID." }); + + const idx = req.user!.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase()); + if (idx === -1) + return res.status(400).json({ errorMessage: "This song is not activated." }); + + req.user?.Library.splice(idx, 1); + req.user?.save(); + + res.json(req.user?.Library); +}) + +App.post("/me/bookmark", RequireAuthentication({ BookmarkedSongs: true }), async (req, res) => { + if (!req.body.SongID) + return res.status(400).json({ errorMessage: "You didn't provide a Song ID." }); + + if (req.user?.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase()) !== -1) + return res.status(400).json({ errorMessage: "This song is already bookmarked." }); + + const SongData = await Song.findOne({ where: { ID: req.body.SongID } }); + if (!SongData) + return res.status(404).json({ errorMessage: "Provided song doesn't exist." }); + + req.user?.BookmarkedSongs.push(SongData); + req.user?.save(); + + res.json(req.user?.BookmarkedSongs.map(x => x.Package())); +}) + +App.post("/me/unbookmark", RequireAuthentication(), async (req, res) => { + if (!req.body.SongID) + return res.status(400).json({ errorMessage: "You didn't provide a Song ID." }); + + const idx = req.user!.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase()); + if (idx === -1) + return res.status(400).json({ errorMessage: "This song is not bookmarked." }); + + req.user?.BookmarkedSongs.splice(idx, 1); + req.user?.save(); + + 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 } }); + if (!SongData) + return res.status(404).json({ errorMessage: "Song not found." }); + + res.json(SongData.Package()); +}) + +App.get("/available", (__, res) => res.json(Object.values(OriginalSparks!).filter(x => !!x.track).map(x => { return { Name: x.track.tt, Template: x.track.ti.substring(11) }; }).sort((a, b) => a.Name.toLowerCase() > b.Name.toLowerCase() ? 1 : -1))) + export default { App, DefaultAPI: "/api/library" diff --git a/Server/Source/Routes/Pages.ts b/Server/Source/Routes/Pages.ts index 7f28c65..607a5e3 100644 --- a/Server/Source/Routes/Pages.ts +++ b/Server/Source/Routes/Pages.ts @@ -1,31 +1,46 @@ import { Router } from "express"; -import { CachedSongFortnitePages } from "../Modules/PagesUtil"; // make sure to import pagesutil after festivalutil :fire: -import { FullFortnitePages } from "../Modules/FNUtil"; +import { FullFortnitePages, GenerateFortnitePages } from "../Modules/FNUtil"; import axios from "axios"; import { IS_DEBUG } from "../Modules/Constants"; +import { RequireAuthentication } from "../Modules/Middleware"; const App = Router(); -App.get("/content/api/pages/fortnite-game", (_, res) => res.json(FullFortnitePages)) +App.get("/content/api/pages/fortnite-game", (_, res) => res.json({ + ...FullFortnitePages, + sparkTracks: { + ...FullFortnitePages!.sparkTracks, + _activeDate: "2023-01-01T01:00:00.000Z", + lastModified: new Date().toISOString() + } +})) -App.get("/content/api/pages/fortnite-game/:Section", async (req, res) => { +App.get("/content/api/pages/fortnite-game/:Section", RequireAuthentication(), async (req, res) => { if (req.params.Section.toLowerCase() === "spark-tracks") // custom song injection + { + const ProcessedPages = await GenerateFortnitePages(req.user!); + + res.removeHeader("Access-Control-Allow-Origin"); + res.removeHeader("Access-Control-Allow-Credentials"); + res.removeHeader("Vary"); + return res.json( { _title: "spark-tracks", _noIndex: false, - _activeDate: "1987-01-01T01:00:00.000Z", // was that the bite of '87 + _activeDate: "2023-01-01T01:00:00.000Z", lastModified: new Date().toISOString(), _locale: "en-US", _templateName: "blank", - ...CachedSongFortnitePages, + ...ProcessedPages.FNPages, _suggestedPrefetch: [] } ); + } const CachedSection = Object.values(FullFortnitePages!).find(x => x._title === req.params.Section); if (!CachedSection) - return res.status(404).json({ error: "funny section not found haha kill me" }); + return res.status(404).json({ errorMessage: "funny section not found haha kill me" }); const ContentFromServer = await axios.get(`https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/${CachedSection._title}`); if (ContentFromServer.status !== 200) diff --git a/Server/Source/Schemas/ForcedCategory.ts b/Server/Source/Schemas/ForcedCategory.ts new file mode 100644 index 0000000..9d0a9ec --- /dev/null +++ b/Server/Source/Schemas/ForcedCategory.ts @@ -0,0 +1,18 @@ +import { BaseEntity, Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm"; +import { Song } from "./Song"; + +@Entity() +export class ForcedCategory extends BaseEntity { + @PrimaryGeneratedColumn("uuid") + ID: string; + + @Column() + Header: string; + + @Column() + Activated: boolean; + + @ManyToMany(() => Song, { eager: true }) + @JoinTable() + Songs: Song[]; +} \ No newline at end of file diff --git a/Server/Source/Schemas/Song.ts b/Server/Source/Schemas/Song.ts index 39e409f..b1cf97a 100644 --- a/Server/Source/Schemas/Song.ts +++ b/Server/Source/Schemas/Song.ts @@ -1,4 +1,5 @@ -import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { BaseEntity, BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { FULL_SERVER_ROOT } from "../Modules/Constants"; @Entity() export class Song extends BaseEntity { @@ -33,8 +34,43 @@ export class Song extends BaseEntity { Tempo: number; @Column() - Cover: string; + Directory: string; + + @Column({ nullable: true }) + Midi?: string; + + @Column({ nullable: true }) + Cover?: string; + + @Column() + BassDifficulty: number; + + @Column() + GuitarDifficulty: number; + + @Column() + DrumsDifficulty: number; + + @Column() + VocalsDifficulty: number; + + @Column() + CreationDate: Date; @Column({ nullable: true }) Lipsync?: string; + + @BeforeInsert() + Setup() { + this.CreationDate = new Date(); + } + + public Package() { + return { + ...this, + Directory: undefined, // we should NOT reveal that + Midi: this.Midi ?? `${FULL_SERVER_ROOT}/song/download/${this.ID}/midi.mid`, + Cover: this.Cover ?? `${FULL_SERVER_ROOT}/song/download/${this.ID}/cover.png` + } + } } \ No newline at end of file diff --git a/Server/Source/Schemas/User.ts b/Server/Source/Schemas/User.ts index e69de29..ec2e71a 100644 --- a/Server/Source/Schemas/User.ts +++ b/Server/Source/Schemas/User.ts @@ -0,0 +1,15 @@ +import { BaseEntity, Column, Entity, JoinTable, ManyToMany, PrimaryColumn } from "typeorm"; +import { Song } from "./Song"; + +@Entity() +export class User extends BaseEntity { + @PrimaryColumn() + ID: string; + + @Column({ type: "simple-json" }) + Library: { SongID: string, Overriding: string }[]; + + @ManyToMany(() => Song, { eager: true }) + @JoinTable() + BookmarkedSongs: Song[]; +} \ No newline at end of file diff --git a/Server/package-lock.json b/Server/package-lock.json index 086b944..5e05acc 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -18,13 +18,15 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "typeorm": "^0.3.19" + "typeorm": "^0.3.19", + "underscore": "^1.13.6" }, "devDependencies": { "@types/cookie-parser": "^1.4.6", "@types/express": "^4.17.18", "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.6.3", + "@types/underscore": "^1.11.15", "tslib": "^2.6.2", "typescript": "^5.2.2" } @@ -178,6 +180,12 @@ "@types/node": "*" } }, + "node_modules/@types/underscore": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz", + "integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2166,6 +2174,11 @@ "node": ">=14.17" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/Server/package.json b/Server/package.json index cc2742b..57df92d 100644 --- a/Server/package.json +++ b/Server/package.json @@ -33,6 +33,7 @@ "@types/express": "^4.17.18", "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.6.3", + "@types/underscore": "^1.11.15", "tslib": "^2.6.2", "typescript": "^5.2.2" }, @@ -46,7 +47,8 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "typeorm": "^0.3.19" + "typeorm": "^0.3.19", + "underscore": "^1.13.6" }, "homepage": "https://github.com/McMistrzYT/BasedServer#readme" } diff --git a/Server/tsconfig.json b/Server/tsconfig.json index 7885afd..af8ccda 100644 --- a/Server/tsconfig.json +++ b/Server/tsconfig.json @@ -77,7 +77,7 @@ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, - "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */, // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ diff --git a/src/App.tsx b/src/App.tsx index 431b069..581ba6c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { AdminTrackList } from "./routes/AdminTrackList"; import { AdminHome } from "./routes/AdminHome"; import { AdminLogin } from "./routes/AdminLogin"; import { Download } from "./routes/Download"; +import { Tracks } from "./routes/Tracks"; import { Profile } from "./routes/Profile"; import { SiteContext, SiteState } from "./utils/State"; import merge from "deepmerge"; @@ -36,6 +37,7 @@ function App() { {/* User-accessible routes */} } /> } /> + } /> } /> {/* Admin routes */} diff --git a/src/components/SiteHeader.tsx b/src/components/SiteHeader.tsx index 08d8c82..c0e3670 100644 --- a/src/components/SiteHeader.tsx +++ b/src/components/SiteHeader.tsx @@ -2,14 +2,14 @@ import { Avatar, Box, Header } from "@primer/react"; import { SignInIcon } from "@primer/octicons-react" import { useCookies } from "react-cookie"; import { useNavigate } from "react-router-dom"; -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect } from "react"; import { SiteContext, UserDetailInterface } from "../utils/State"; import { Buffer } from "buffer/"; import Favicon from "../assets/favicon.webp"; import axios from "axios"; +import { toast } from "react-toastify"; export function SiteHeader() { - const [discordUrl, setDiscordUrl] = useState(""); const {state, setState} = useContext(SiteContext); const [cookies] = useCookies(); const navigate = useNavigate(); @@ -17,22 +17,14 @@ export function SiteHeader() { useEffect(() => { (async () => { const Data = await axios.get("/api/discord/url"); - if (Data.status === 200) - setDiscordUrl(Data.data); + const Details: UserDetailInterface = cookies["UserDetails"] ? JSON.parse(decodeURI(Buffer.from(cookies["UserDetails"], "base64").toString())) : null; + + setState({ + ...state, + UserDetails: Details, + DiscordOauthURL: Data.data + }); })(); - }, []); - - useEffect(() => { - if (!cookies["UserDetails"]) - return; - - const Details: UserDetailInterface = JSON.parse(decodeURI(Buffer.from(cookies["UserDetails"], "base64").toString())); - setState({ - ...state, - UserDetails: Details - }); - - console.log(Details); }, [cookies["UserDetails"]]) return ( @@ -41,18 +33,18 @@ export function SiteHeader() { Partypack - Daily Rotation - Leaderboards - Tracks - Tutorials - FAQ + navigate("/rotation")}>Daily Rotation + navigate("/creators")}>Top Creators + navigate("/tracks")}>Tracks + navigate("/tutorials")}>Tutorials + navigate("/faq")}>FAQ window.open("https://discord.gg/KaxknAbqDS")}>Discord - Download + navigate("/download")}>Download { cookies["AdminKey"] ? navigate("/admin")} sx={{ cursor: "pointer", color: "danger.emphasis" }}>Admin : <> } { cookies["Token"] && state.UserDetails ? navigate("/profile")}> : - discordUrl ? window.location.assign(discordUrl) : console.log("no discord url :(")}> + state.DiscordOauthURL ? window.location.assign(state.DiscordOauthURL) : toast("Cannot redirect to login. No Discord OAuth URL has been received from the backend.", { type: "error" })}> } ); diff --git a/src/components/Song.tsx b/src/components/Song.tsx index 0980b79..0d27b6d 100644 --- a/src/components/Song.tsx +++ b/src/components/Song.tsx @@ -1,13 +1,13 @@ import { Box, Text } from "@primer/react"; import { Divider } from "@primer/react/lib-esm/ActionList/Divider"; -export function Song({ children }: { children: JSX.Element[] }) { +export function Song({ data, children }: { data: any, children: JSX.Element[] | JSX.Element | string }) { return ( - +
- Someone - Someone's Song Someone's Song + {data.ArtistName} + {data.Name}
{children} diff --git a/src/css/index.css b/src/css/index.css index f48d3a3..85c72af 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -1,8 +1,14 @@ .content { padding: 10px; + margin-left: 15%; + margin-right: 15%; } .songCategory { display: inline-flex; gap: 10px; +} + +#__primerPortalRoot__ > div { + z-index: 1000 !important; } \ No newline at end of file diff --git a/src/routes/AdminTrackList.tsx b/src/routes/AdminTrackList.tsx index f27a6ec..463a8b3 100644 --- a/src/routes/AdminTrackList.tsx +++ b/src/routes/AdminTrackList.tsx @@ -1,9 +1,47 @@ -import { Text } from "@primer/react"; +import axios from "axios"; +import { Box, Button, Heading } from "@primer/react"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { Song } from "../components/Song"; +import { useNavigate } from "react-router-dom"; export function AdminTrackList() { + const [tracks, setTracks] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + (async () => { + const Tracks = await axios.get("/admin/api/tracks"); + if (Tracks.status !== 200) + return toast("Error while requesting tracks!"); + + setTracks(Tracks.data); + })(); + }, []); + return ( <> - Track list + All tracks (admin) + + { + tracks.map(x => { + return + + + + + }) + } + ) } \ No newline at end of file diff --git a/src/routes/Profile.tsx b/src/routes/Profile.tsx index dcb74c3..3c5d1e0 100644 --- a/src/routes/Profile.tsx +++ b/src/routes/Profile.tsx @@ -1,59 +1,136 @@ -import { Avatar, Box, Button, Heading, Text } from "@primer/react" +import { ActionList, ActionMenu, Avatar, Box, Button, Dialog, Heading, Text } from "@primer/react" import { Divider } from "@primer/react/lib-esm/ActionList/Divider"; import { PageHeader } from "@primer/react/drafts"; -import { useContext } from "react"; +import { useContext, useEffect, useState } from "react"; import { SiteContext } from "../utils/State"; import { useCookies } from "react-cookie"; import { useNavigate } from "react-router-dom"; import { Song } from "../components/Song"; +import axios from "axios"; +import { toast } from "react-toastify"; export function Profile() { - const { state, setState } = useContext(SiteContext); - const [, , removeCookie] = useCookies(); - const navigate = useNavigate(); + const { state, setState } = useContext(SiteContext); + const [, , removeCookie] = useCookies(); + const [isActivateDialogOpen, setIsActivateDialogOpen] = useState(false); + const [librarySongs, setLibrarySongs] = useState([]); + const [bookmarkedSongs, setBookmarkedSongs] = useState([]); + const [availableOverrides, setAvailableOverrides] = useState<{ Name: string, Template: string }[]>([]); + const [overriding, setOverriding] = useState({}); + const navigate = useNavigate(); - return ( - <> - { - state.UserDetails ? - - - - - - - - {state.UserDetails.GlobalName} (@{state.UserDetails.Username}) - - - - - - - - Active Songs - - -
Overriding: Party Rock Anthem
- -
-
- My Bookmarks - - - - - - - - - - -
: - <> - You are not logged in.
Log in using the button in the top right.
- - } - - ) + useEffect(() => { + (async () => { + const Data = await axios.get("/api/library/me"); + const Overrides = await axios.get("/api/library/available"); + + if (Data.status !== 200 || Overrides.status !== 200) + 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); + setAvailableOverrides(Overrides.data); + })(); + }, []); + + return ( + <> + { + state.UserDetails ? + + + + + + + + {state.UserDetails.GlobalName} (@{state.UserDetails.Username}) + + + + + + + + Active Songs + setIsActivateDialogOpen(false)} aria-labelledby="header"> + Activate song + + In order to activate a song for use, you need to sacrifice another song.
Please select a song you own to replace:
+ + Select a song... + + + { + availableOverrides.map(x => { + return { + setIsActivateDialogOpen(false); + const Res = await axios.post("/api/library/me/activate", { SongID: overriding!.ID!, ToOverride: x.Template }); + if (Res.status === 200) + setLibrarySongs([ + ...librarySongs, + { ...overriding!, Override: x.Template } + ]) + else + toast(Res.data.errorMessage, { type: "error" }) + }}> + {x.Name} + ; + }) + } + + + +
+
+ + { + librarySongs.length >= 1 ? + librarySongs.map(x => { + return +
Overriding: {availableOverrides.find(y => y.Template.toLowerCase() === x.Override.toLowerCase())?.Name}
+ +
; + }) + : You have no activated songs. + } +
+ My Bookmarks + + { + bookmarkedSongs.length >= 1 ? + bookmarkedSongs.map(x => { + return + + + ; + }) + : You have no bookmarked songs. + } + +
: + <> + You are not logged in.
Log in using the button in the top right.
+ + } + + ) } \ No newline at end of file diff --git a/src/routes/Tracks.tsx b/src/routes/Tracks.tsx new file mode 100644 index 0000000..75c836c --- /dev/null +++ b/src/routes/Tracks.tsx @@ -0,0 +1,79 @@ +import { Box, Button, Heading } from "@primer/react"; +import { Song } from "../components/Song"; +import { useContext, useEffect, useState } from "react"; +import axios from "axios"; +import { toast } from "react-toastify"; +import { SiteContext } from "../utils/State"; + +export function Tracks() { + const [trackData, setTrackData] = useState<{ Header: string, Songs: unknown[] }[]>([]); + const [bookmarks, setBookmarks] = useState([]); + const {state} = useContext(SiteContext); + + useEffect(() => { + (async () => { + const Discovery = await axios.get("/api/discovery"); + if (Discovery.status !== 200) + return toast(Discovery.data.errorMessage, { type: "error" }); + + setTrackData(Discovery.data); + + const Bookmarks = await axios.get("/api/library/me"); + if (Bookmarks.status !== 200) + return// toast(Bookmarks.data.errorMessage, { type: "error" }); + + setBookmarks(Bookmarks.data.Bookmarks); + })(); + }, []) + + return ( + <> + { + trackData.map(x => { + return + {x.Header} + + { + x.Songs.map(x => { + return + { + bookmarks.findIndex(y => y.ID === x.ID) !== -1 ? + : + + } + + }) + } + + + }) + } + + ) +} \ No newline at end of file diff --git a/src/utils/State.ts b/src/utils/State.ts index 3aaf1be..b0f9f0d 100644 --- a/src/utils/State.ts +++ b/src/utils/State.ts @@ -6,7 +6,8 @@ export const SiteContext = createContext({} as IContext); export interface UserDetailInterface { ID: string, Username: string, GlobalName: string, Avatar: string } export interface SiteState { - UserDetails: UserDetailInterface | null + UserDetails: UserDetailInterface | null; + DiscordOauthURL: string | null; } export interface IContext { diff --git a/tsconfig.json b/tsconfig.json index a7fc6fb..7891d62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ /* Linting */ "strict": true, + "noImplicitAny": false, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true