as always - MANY changes and stuff works now :)
This commit is contained in:
parent
8d4436a123
commit
8d95389b6a
|
@ -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"
|
|
||||||
}
|
|
Binary file not shown.
BIN
Server/Saved/Songs/ButterBarnHoedown/Cover.png
Normal file
BIN
Server/Saved/Songs/ButterBarnHoedown/Cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 525 KiB |
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00001.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00001.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00002.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00002.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00003.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00003.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00004.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00004.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00005.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00005.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00006.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00006.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00007.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00007.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00008.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/chunk-stream0-00008.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Chunks/init-stream0.m4s
Normal file
BIN
Server/Saved/Songs/Dogsong/Chunks/init-stream0.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Cover.png
Normal file
BIN
Server/Saved/Songs/Dogsong/Cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
Server/Saved/Songs/Dogsong/Data.mid
Normal file
BIN
Server/Saved/Songs/Dogsong/Data.mid
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Dogsong/Dogsong.m4a
Normal file
BIN
Server/Saved/Songs/Dogsong/Dogsong.m4a
Normal file
Binary file not shown.
31
Server/Saved/Songs/Dogsong/Manifest.mpd
Normal file
31
Server/Saved/Songs/Dogsong/Manifest.mpd
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="urn:mpeg:dash:schema:mpd:2011"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
|
||||||
|
profiles="urn:mpeg:dash:profile:isoff-live:2011"
|
||||||
|
type="static"
|
||||||
|
mediaPresentationDuration="PT37.3S"
|
||||||
|
maxSegmentDuration="PT5.0S"
|
||||||
|
minBufferTime="PT10.0S">
|
||||||
|
<BaseURL>{BASEURL}</BaseURL>
|
||||||
|
<ProgramInformation>
|
||||||
|
<Title>Dogsong</Title>
|
||||||
|
</ProgramInformation>
|
||||||
|
<ServiceDescription id="0">
|
||||||
|
</ServiceDescription>
|
||||||
|
<Period id="0" start="PT0.0S">
|
||||||
|
<AdaptationSet id="0" contentType="audio" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true" lang="und">
|
||||||
|
<Representation id="0" mimeType="audio/mp4" codecs="mp4a.40.2" bandwidth="128000" audioSamplingRate="48000">
|
||||||
|
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
|
||||||
|
<SegmentTemplate timescale="48000" initialization="init-stream$RepresentationID$.m4s" media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
|
||||||
|
<SegmentTimeline>
|
||||||
|
<S t="0" d="239616" />
|
||||||
|
<S d="240640" r="5" />
|
||||||
|
<S d="106524" />
|
||||||
|
</SegmentTimeline>
|
||||||
|
</SegmentTemplate>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
BIN
Server/Saved/Songs/Zombies/Audio.m4a
Normal file
BIN
Server/Saved/Songs/Zombies/Audio.m4a
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00001.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00001.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00002.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00002.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00003.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00003.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00004.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00004.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00005.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00005.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00006.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00006.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00007.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00007.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00008.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00008.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00009.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00009.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00010.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00010.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00011.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00011.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00012.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00012.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00013.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00013.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00014.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00014.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00015.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00015.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00016.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00016.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00017.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00017.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00018.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00018.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00019.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00019.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00020.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00020.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00021.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00021.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00022.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00022.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00023.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00023.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00024.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00024.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00025.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00025.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00026.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00026.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00027.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00027.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00028.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00028.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00029.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00029.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00030.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00030.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00031.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00031.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00032.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00032.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00033.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/chunk-stream0-00033.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Chunks/init-stream0.m4s
Normal file
BIN
Server/Saved/Songs/Zombies/Chunks/init-stream0.m4s
Normal file
Binary file not shown.
BIN
Server/Saved/Songs/Zombies/Cover.png
Normal file
BIN
Server/Saved/Songs/Zombies/Cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 752 KiB |
BIN
Server/Saved/Songs/Zombies/Data.mid
Normal file
BIN
Server/Saved/Songs/Zombies/Data.mid
Normal file
Binary file not shown.
30
Server/Saved/Songs/Zombies/Manifest.mpd
Normal file
30
Server/Saved/Songs/Zombies/Manifest.mpd
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="urn:mpeg:dash:schema:mpd:2011"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
|
||||||
|
profiles="urn:mpeg:dash:profile:isoff-live:2011"
|
||||||
|
type="static"
|
||||||
|
mediaPresentationDuration="PT2M45.2S"
|
||||||
|
maxSegmentDuration="PT5.0S"
|
||||||
|
minBufferTime="PT10.0S">
|
||||||
|
<BaseURL>{BASEURL}</BaseURL>
|
||||||
|
<ProgramInformation>
|
||||||
|
</ProgramInformation>
|
||||||
|
<ServiceDescription id="0">
|
||||||
|
</ServiceDescription>
|
||||||
|
<Period id="0" start="PT0.0S">
|
||||||
|
<AdaptationSet id="0" contentType="audio" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true" lang="und">
|
||||||
|
<Representation id="0" mimeType="audio/mp4" codecs="mp4a.40.2" bandwidth="128000" audioSamplingRate="48000">
|
||||||
|
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
|
||||||
|
<SegmentTemplate timescale="48000" initialization="init-stream$RepresentationID$.m4s" media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
|
||||||
|
<SegmentTimeline>
|
||||||
|
<S t="0" d="239616" />
|
||||||
|
<S d="240640" r="30" />
|
||||||
|
<S d="231738" />
|
||||||
|
</SegmentTimeline>
|
||||||
|
</SegmentTemplate>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
|
@ -1,7 +1,7 @@
|
||||||
import e from "express"
|
import e from "express"
|
||||||
import fs from "fs";
|
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 { 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 { italic, magenta, red, yellow } from "colorette";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import path from "path";
|
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)
|
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!`);
|
return res.status(403).send(`${SERVER_URL} is currently locked behind authentication. Come back later!`);
|
||||||
|
|
||||||
|
Debug(req.path);
|
||||||
next();
|
next();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -41,6 +42,8 @@ async function Initialize() {
|
||||||
|
|
||||||
Msg(`Loaded route ${italic(File)}!`);
|
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)") : "")}`));
|
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();
|
Initialize();
|
||||||
|
|
||||||
// ! FESTIVAL-SPECIFIC STUFF
|
// ! FESTIVAL-SPECIFIC STUFF
|
||||||
// set up both utils for usage
|
|
||||||
import { LoadSongs } from "../Modules/FestivalUtil";
|
|
||||||
import { CacheFortnitePages } from "../Modules/PagesUtil";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
axios.defaults.validateStatus = () => true;
|
axios.defaults.validateStatus = () => true;
|
||||||
LoadSongs();
|
axios.defaults.headers.common["X-Do-Not-Redirect"] = "true";
|
||||||
CacheFortnitePages();
|
|
|
@ -2,31 +2,64 @@ import axios from "axios";
|
||||||
import { Err } from "./Logger";
|
import { Err } from "./Logger";
|
||||||
import { red } from "colorette";
|
import { red } from "colorette";
|
||||||
import { FULL_SERVER_ROOT } from "./Constants";
|
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
|
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
|
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 ?
|
FullFortnitePages === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000 ?
|
||||||
await axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game") :
|
await axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game") :
|
||||||
{ status: 200, data: FullFortnitePages };
|
{ 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();
|
LastContentDownloadDate = new Date();
|
||||||
|
|
||||||
if (status !== 200) {
|
if (!ForUser)
|
||||||
Err(`Failed to get Fortnite pages: ${red(status)}`);
|
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);
|
console.log(data);
|
||||||
process.exit(-1); // very big fuck moment, we literally cannot run the server without fortnitepages
|
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
|
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] = {
|
if (!Song)
|
||||||
_title: Song.UUID,
|
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,
|
_noIndex: false,
|
||||||
_activeDate: "2023-01-01T01:00:00.000Z",
|
_activeDate: "2023-01-01T01:00:00.000Z",
|
||||||
_locale: "en-US",
|
_locale: "en-US",
|
||||||
|
@ -34,37 +67,37 @@ export async function GenerateFortnitePages(): Promise<{ Success: boolean, FNPag
|
||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
track: {
|
track: {
|
||||||
tt: Song.Name, // tt - Title,
|
tt: Song.Name, // tt - Title,
|
||||||
an: Song.Artist, // an - Artist Name
|
an: Song.ArtistName, // an - Artist Name
|
||||||
mm: Song.MinorMajor, // mm - Minor, Major
|
mm: Song.Scale, // mm - Minor, Major
|
||||||
mk: Song.Key, // mk - Music Key
|
mk: Song.Key, // mk - Music Key
|
||||||
ab: Song.Album, // ab - Album
|
ab: Song.Album, // ab - Album
|
||||||
su: Song.UUID, // su - Song UUID
|
su: OriginalTrack._title, // su - Song UUID
|
||||||
ry: Song.Year, // ry - Release Year
|
ry: Song.Year, // ry - Release Year
|
||||||
mt: Song.BeatsPerMinute, // mt - Music Timing (?)
|
mt: Song.Tempo, // mt - Music Timing (?)
|
||||||
au: Song.Cover ?? `${FULL_SERVER_ROOT}/song/download/${Song.UUID}/cover.png`, // au - Album Cover
|
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)
|
gt: [ "Jam-LoopIsUnpitched-Beat" ], // gt - Gameplay Tags (but in a different format: Example.Gameplay.Tag -> Example-Gameplay-Tag)
|
||||||
ti: `SparksSong:${Song.AssetID.toLowerCase()}`,
|
ti: `SparksSong:${OverridingAs.Overriding.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)
|
mu: Song.Midi ?? `${FULL_SERVER_ROOT}/song/download/${Song.ID}/midi.mid`, // mu - Song Midi (encrypted)
|
||||||
dn: Song.Length, // dn - Track Length (in seconds)
|
dn: Song.Length, // dn - Track Length (in seconds)
|
||||||
ge: [ "Pop" ], // ge - Genres
|
ge: [ "Pop" ], // ge - Genres
|
||||||
in: { // TODO: fuck with this to make difficulties :+1:
|
in: {
|
||||||
ba: 1,
|
ba: Song.BassDifficulty,
|
||||||
pb: 2,
|
pb: Song.BassDifficulty,
|
||||||
pd: 3,
|
pd: Song.DrumsDifficulty,
|
||||||
pg: 4,
|
ds: Song.DrumsDifficulty,
|
||||||
vl: 3,
|
pg: Song.GuitarDifficulty,
|
||||||
ds: 2,
|
gr: Song.GuitarDifficulty,
|
||||||
gr: 1,
|
vl: Song.VocalsDifficulty,
|
||||||
_type: "SparkTrackIntensities"
|
_type: "SparkTrackIntensities"
|
||||||
}, // in - Intensities (those white bars you see)
|
}, // in - Intensities (those white bars you see)
|
||||||
sib: "Bass", // sib - Bass ID to use (only Bass possible)
|
sib: "Bass", // sib - Bass ID to use (only Bass possible)
|
||||||
sid: "Drum", // sid - Drums ID to use (only Drum 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)
|
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)
|
qi: JSON.stringify({ // qi - Query Information (frontend related display stuff and language vocals channel related stuff)
|
||||||
sid: Song.UUID, // sid - Song UUID
|
sid: Song.ID, // sid - Song UUID
|
||||||
pid: Song.UUID, // pid - Playlist Asset ID
|
pid: Song.ID, // pid - Playlist Asset ID
|
||||||
title: Song.UUID, // title - Song Name - same as _title
|
title: OriginalTrack._title, // title - Song Name - same as _title
|
||||||
tracks: [
|
tracks: [
|
||||||
{
|
{
|
||||||
part: "ds", // Drum Set
|
part: "ds", // Drum Set
|
||||||
|
@ -87,18 +120,18 @@ export async function GenerateFortnitePages(): Promise<{ Success: boolean, FNPag
|
||||||
vols: [ 4, 4 ]
|
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" ],
|
channels: [ "FL", "FR" ],
|
||||||
vols: [ 4, 4 ]
|
vols: [ 4, 4 ]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
preview: {
|
preview: {
|
||||||
starttime: Song.PreviewTime
|
starttime: 0
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
ld: Song.LipsyncData, // ld - Lipsync Data (it's literally a uasset)
|
ld: Song.Lipsync ?? OriginalTrack.track.ld, // ld - Lipsync Data (it's literally a uasset)
|
||||||
jc: Song.JoinCode, // jc - Join Code (UEFN empty island with nothing - possibly downloads assets)
|
jc: OriginalTrack.track.jc, // jc - Join Code (UEFN empty island with nothing - possibly downloads assets)
|
||||||
sn: Song.UUID, // sn - Song Name - same as _title
|
sn: OriginalTrack._title, // sn - Song Name - same as _title
|
||||||
_type: "SparkTrack"
|
_type: "SparkTrack"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
|
||||||
})
|
|
35
Server/Source/Modules/Middleware.ts
Normal file
35
Server/Source/Modules/Middleware.ts
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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!");
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { ADMIN_KEY } from "../Modules/Constants";
|
import { ADMIN_KEY } from "../Modules/Constants";
|
||||||
|
import { Song } from "../Schemas/Song";
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ App.use((req, res, next) => {
|
||||||
if (req.path === "/key")
|
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.");
|
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.");
|
return res.status(403).send("You don't have permission to access this endpoint.");
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
@ -19,6 +20,10 @@ App.use((req, res, next) => {
|
||||||
|
|
||||||
App.get("/test", (_, res) => res.send("Permission check OK"));
|
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 {
|
export default {
|
||||||
App,
|
App,
|
||||||
DefaultAPI: "/admin/api"
|
DefaultAPI: "/admin/api"
|
||||||
|
|
|
@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
||||||
import qs from "querystring";
|
import qs from "querystring";
|
||||||
import { Response, Router } from "express";
|
import { Response, Router } from "express";
|
||||||
import { DASHBOARD_ROOT, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, FULL_SERVER_ROOT, JWT_KEY } from "../Modules/Constants";
|
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();
|
const App = Router();
|
||||||
|
|
||||||
|
@ -35,6 +36,12 @@ App.get("/discord", async (req, res) => {
|
||||||
|
|
||||||
await QuickRevokeToken(res, Discord.data.access_token);
|
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
|
res
|
||||||
.cookie("Token", jwt.sign({ ID: UserData.data.id }, JWT_KEY!, { algorithm: "HS256" }))
|
.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"))
|
.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"))
|
||||||
|
|
24
Server/Source/Routes/Discovery.ts
Normal file
24
Server/Source/Routes/Discovery.ts
Normal file
|
@ -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"
|
||||||
|
}
|
|
@ -1,17 +1,18 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { existsSync, readFileSync } from "fs";
|
import { existsSync, readFileSync } from "fs";
|
||||||
import { AvailableFestivalSongs } from "../Modules/FestivalUtil";
|
|
||||||
import { FULL_SERVER_ROOT } from "../Modules/Constants";
|
import { FULL_SERVER_ROOT } from "../Modules/Constants";
|
||||||
import { CreateBlurl } from "../Modules/BLURL";
|
import { CreateBlurl } from "../Modules/BLURL";
|
||||||
|
import { Song } from "../Schemas/Song";
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
|
|
||||||
App.get("/song/download/:SongUUID/:File", (req, res) => {
|
App.get("/song/download/:InternalID/:File", async (req, res) => {
|
||||||
const Song = AvailableFestivalSongs.find(x => x.UUID === req.params.SongUUID);
|
//const Song = AvailableFestivalSongs.find(x => x.UUID === req.params.SongUUID);
|
||||||
if (!Song)
|
const SongData = await Song.findOne({ where: { ID: req.params.InternalID } });
|
||||||
return res.sendStatus(404);
|
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()) {
|
switch (req.params.File.toLowerCase()) {
|
||||||
case "master.blurl":
|
case "master.blurl":
|
||||||
case "main.blurl":
|
case "main.blurl":
|
||||||
|
@ -22,7 +23,7 @@ App.get("/song/download/:SongUUID/:File", (req, res) => {
|
||||||
type: "main",
|
type: "main",
|
||||||
language: "en",
|
language: "en",
|
||||||
url: `${BaseURL}master.blurl`,
|
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",
|
type: "vod",
|
||||||
|
@ -32,11 +33,11 @@ App.get("/song/download/:SongUUID/:File", (req, res) => {
|
||||||
|
|
||||||
case "manifest":
|
case "manifest":
|
||||||
case "manifest.mpd":
|
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":
|
||||||
case "cover.png":
|
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
|
// ! we are not risking a lawsuit
|
||||||
//case "midi.dat": // dont forget to encrypt!
|
//case "midi.dat": // dont forget to encrypt!
|
||||||
|
@ -46,7 +47,7 @@ App.get("/song/download/:SongUUID/:File", (req, res) => {
|
||||||
case "midi":
|
case "midi":
|
||||||
case "midi.mid":
|
case "midi.mid":
|
||||||
case "midi.midi": // forget to encrypt!
|
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))
|
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"))
|
if (!req.params.File.endsWith(".m4s"))
|
||||||
return res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
|
|
||||||
if (!existsSync(`${Song.Directory}/Chunks/${req.params.File}`))
|
if (!existsSync(`${SongData.Directory}/Chunks/${req.params.File}`))
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
|
|
||||||
res.set("content-type", "video/mp4")
|
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 {
|
export default {
|
||||||
App
|
App
|
||||||
|
|
|
@ -1,13 +1,91 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
import { RequireAuthentication } from "../Modules/Middleware";
|
||||||
|
import { Song } from "../Schemas/Song";
|
||||||
|
import { OriginalSparks } from "../Modules/FNUtil";
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
|
|
||||||
App.get("/song/data/:InternalID", (req, res) => {
|
App.get("/me", RequireAuthentication({ BookmarkedSongs: true }), (req, res) => {
|
||||||
res.json({
|
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_<number>\"" });
|
||||||
|
|
||||||
|
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 {
|
export default {
|
||||||
App,
|
App,
|
||||||
DefaultAPI: "/api/library"
|
DefaultAPI: "/api/library"
|
||||||
|
|
|
@ -1,31 +1,46 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { CachedSongFortnitePages } from "../Modules/PagesUtil"; // make sure to import pagesutil after festivalutil :fire:
|
import { FullFortnitePages, GenerateFortnitePages } from "../Modules/FNUtil";
|
||||||
import { FullFortnitePages } from "../Modules/FNUtil";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { IS_DEBUG } from "../Modules/Constants";
|
import { IS_DEBUG } from "../Modules/Constants";
|
||||||
|
import { RequireAuthentication } from "../Modules/Middleware";
|
||||||
|
|
||||||
const App = Router();
|
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
|
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(
|
return res.json(
|
||||||
{
|
{
|
||||||
_title: "spark-tracks",
|
_title: "spark-tracks",
|
||||||
_noIndex: false,
|
_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(),
|
lastModified: new Date().toISOString(),
|
||||||
_locale: "en-US",
|
_locale: "en-US",
|
||||||
_templateName: "blank",
|
_templateName: "blank",
|
||||||
...CachedSongFortnitePages,
|
...ProcessedPages.FNPages,
|
||||||
_suggestedPrefetch: []
|
_suggestedPrefetch: []
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CachedSection = Object.values(FullFortnitePages!).find(x => x._title === req.params.Section);
|
const CachedSection = Object.values(FullFortnitePages!).find(x => x._title === req.params.Section);
|
||||||
if (!CachedSection)
|
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}`);
|
const ContentFromServer = await axios.get(`https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/${CachedSection._title}`);
|
||||||
if (ContentFromServer.status !== 200)
|
if (ContentFromServer.status !== 200)
|
||||||
|
|
18
Server/Source/Schemas/ForcedCategory.ts
Normal file
18
Server/Source/Schemas/ForcedCategory.ts
Normal file
|
@ -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[];
|
||||||
|
}
|
|
@ -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()
|
@Entity()
|
||||||
export class Song extends BaseEntity {
|
export class Song extends BaseEntity {
|
||||||
|
@ -33,8 +34,43 @@ export class Song extends BaseEntity {
|
||||||
Tempo: number;
|
Tempo: number;
|
||||||
|
|
||||||
@Column()
|
@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 })
|
@Column({ nullable: true })
|
||||||
Lipsync?: string;
|
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`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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[];
|
||||||
|
}
|
15
Server/package-lock.json
generated
15
Server/package-lock.json
generated
|
@ -18,13 +18,15 @@
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"typeorm": "^0.3.19"
|
"typeorm": "^0.3.19",
|
||||||
|
"underscore": "^1.13.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie-parser": "^1.4.6",
|
"@types/cookie-parser": "^1.4.6",
|
||||||
"@types/express": "^4.17.18",
|
"@types/express": "^4.17.18",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/node": "^20.6.3",
|
"@types/node": "^20.6.3",
|
||||||
|
"@types/underscore": "^1.11.15",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
|
@ -178,6 +180,12 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
@ -2166,6 +2174,11 @@
|
||||||
"node": ">=14.17"
|
"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": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"@types/express": "^4.17.18",
|
"@types/express": "^4.17.18",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/node": "^20.6.3",
|
"@types/node": "^20.6.3",
|
||||||
|
"@types/underscore": "^1.11.15",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
|
@ -46,7 +47,8 @@
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"typeorm": "^0.3.19"
|
"typeorm": "^0.3.19",
|
||||||
|
"underscore": "^1.13.6"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/McMistrzYT/BasedServer#readme"
|
"homepage": "https://github.com/McMistrzYT/BasedServer#readme"
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
/* Type Checking */
|
/* Type Checking */
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
"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'. */,
|
"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. */
|
// "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. */
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { AdminTrackList } from "./routes/AdminTrackList";
|
||||||
import { AdminHome } from "./routes/AdminHome";
|
import { AdminHome } from "./routes/AdminHome";
|
||||||
import { AdminLogin } from "./routes/AdminLogin";
|
import { AdminLogin } from "./routes/AdminLogin";
|
||||||
import { Download } from "./routes/Download";
|
import { Download } from "./routes/Download";
|
||||||
|
import { Tracks } from "./routes/Tracks";
|
||||||
import { Profile } from "./routes/Profile";
|
import { Profile } from "./routes/Profile";
|
||||||
import { SiteContext, SiteState } from "./utils/State";
|
import { SiteContext, SiteState } from "./utils/State";
|
||||||
import merge from "deepmerge";
|
import merge from "deepmerge";
|
||||||
|
@ -36,6 +37,7 @@ function App() {
|
||||||
{/* User-accessible routes */}
|
{/* User-accessible routes */}
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/download" element={<Download />} />
|
<Route path="/download" element={<Download />} />
|
||||||
|
<Route path="/tracks" element={<Tracks />} />
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
|
|
||||||
{/* Admin routes */}
|
{/* Admin routes */}
|
||||||
|
|
|
@ -2,14 +2,14 @@ import { Avatar, Box, Header } from "@primer/react";
|
||||||
import { SignInIcon } from "@primer/octicons-react"
|
import { SignInIcon } from "@primer/octicons-react"
|
||||||
import { useCookies } from "react-cookie";
|
import { useCookies } from "react-cookie";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { SiteContext, UserDetailInterface } from "../utils/State";
|
import { SiteContext, UserDetailInterface } from "../utils/State";
|
||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import Favicon from "../assets/favicon.webp";
|
import Favicon from "../assets/favicon.webp";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
const [discordUrl, setDiscordUrl] = useState("");
|
|
||||||
const {state, setState} = useContext(SiteContext);
|
const {state, setState} = useContext(SiteContext);
|
||||||
const [cookies] = useCookies();
|
const [cookies] = useCookies();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -17,22 +17,14 @@ export function SiteHeader() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const Data = await axios.get("/api/discord/url");
|
const Data = await axios.get("/api/discord/url");
|
||||||
if (Data.status === 200)
|
const Details: UserDetailInterface = cookies["UserDetails"] ? JSON.parse(decodeURI(Buffer.from(cookies["UserDetails"], "base64").toString())) : null;
|
||||||
setDiscordUrl(Data.data);
|
|
||||||
|
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"]])
|
}, [cookies["UserDetails"]])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -41,18 +33,18 @@ export function SiteHeader() {
|
||||||
<img src={Favicon} style={{ width: 32, height: "auto", paddingRight: 5 }} />
|
<img src={Favicon} style={{ width: 32, height: "auto", paddingRight: 5 }} />
|
||||||
<b>Partypack</b>
|
<b>Partypack</b>
|
||||||
</Header.Item>
|
</Header.Item>
|
||||||
<Header.Item full sx={{ cursor: "pointer" }}>Daily Rotation</Header.Item>
|
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/rotation")}>Daily Rotation</Header.Item>
|
||||||
<Header.Item full sx={{ cursor: "pointer" }}>Leaderboards</Header.Item>
|
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/creators")}>Top Creators</Header.Item>
|
||||||
<Header.Item full sx={{ cursor: "pointer" }}>Tracks</Header.Item>
|
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/tracks")}>Tracks</Header.Item>
|
||||||
<Header.Item full sx={{ cursor: "pointer" }}>Tutorials</Header.Item>
|
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/tutorials")}>Tutorials</Header.Item>
|
||||||
<Header.Item full sx={{ cursor: "pointer" }}>FAQ</Header.Item>
|
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/faq")}>FAQ</Header.Item>
|
||||||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => window.open("https://discord.gg/KaxknAbqDS")}>Discord</Header.Item>
|
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => window.open("https://discord.gg/KaxknAbqDS")}>Discord</Header.Item>
|
||||||
<Header.Item full sx={{ cursor: "pointer", color: "accent.emphasis" }}>Download</Header.Item>
|
<Header.Item full sx={{ cursor: "pointer", color: "accent.emphasis" }} onClick={() => navigate("/download")}>Download</Header.Item>
|
||||||
{ cookies["AdminKey"] ? <Header.Item full onClick={() => navigate("/admin")} sx={{ cursor: "pointer", color: "danger.emphasis" }}>Admin</Header.Item> : <></> }
|
{ cookies["AdminKey"] ? <Header.Item full onClick={() => navigate("/admin")} sx={{ cursor: "pointer", color: "danger.emphasis" }}>Admin</Header.Item> : <></> }
|
||||||
{
|
{
|
||||||
cookies["Token"] && state.UserDetails ?
|
cookies["Token"] && state.UserDetails ?
|
||||||
<Header.Item sx={{ mr: 0, cursor: "pointer" }} onClick={() => navigate("/profile")}><Avatar src={state.UserDetails.Avatar} size={25} alt={`${state.UserDetails.GlobalName} (@${state.UserDetails.Username})`}/></Header.Item> :
|
<Header.Item sx={{ mr: 0, cursor: "pointer" }} onClick={() => navigate("/profile")}><Avatar src={state.UserDetails.Avatar} size={25} alt={`${state.UserDetails.GlobalName} (@${state.UserDetails.Username})`}/></Header.Item> :
|
||||||
<Box sx={{ cursor: "pointer" }} onClick={() => discordUrl ? window.location.assign(discordUrl) : console.log("no discord url :(")}><SignInIcon size={16} /></Box>
|
<Box sx={{ cursor: "pointer" }} onClick={() => state.DiscordOauthURL ? window.location.assign(state.DiscordOauthURL) : toast("Cannot redirect to login. No Discord OAuth URL has been received from the backend.", { type: "error" })}><SignInIcon size={16} /></Box>
|
||||||
}
|
}
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Box, Text } from "@primer/react";
|
import { Box, Text } from "@primer/react";
|
||||||
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
|
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 (
|
return (
|
||||||
<Box sx={{ overflow: "hidden", minWidth: 50, maxWidth: 200, padding: 2, borderRadius: 10, border: "solid", borderColor: "border.default" }}>
|
<Box sx={{ overflow: "hidden", minWidth: 50, maxWidth: 200, padding: 2, borderRadius: 10, border: "solid", borderColor: "border.default" }}>
|
||||||
<img src="https://cdn2.unrealengine.com/d5yc9zpe97um68u6-512x512-a7f5fc0d3c2f.png" style={{ width: "100%", borderRadius: 10 }} />
|
<img src={data.Cover} style={{ width: "100%", borderRadius: 10 }} />
|
||||||
<center>
|
<center>
|
||||||
<Text sx={{ fontSize: 1, display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>Someone</Text>
|
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{data.ArtistName}</Text>
|
||||||
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>Someone's Song Someone's Song</Text>
|
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{data.Name}</Text>
|
||||||
<Divider />
|
<Divider />
|
||||||
</center>
|
</center>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
.content {
|
.content {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
margin-left: 15%;
|
||||||
|
margin-right: 15%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.songCategory {
|
.songCategory {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#__primerPortalRoot__ > div {
|
||||||
|
z-index: 1000 !important;
|
||||||
}
|
}
|
|
@ -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() {
|
export function AdminTrackList() {
|
||||||
|
const [tracks, setTracks] = useState<unknown[]>([]);
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text>Track list</Text>
|
<Heading>All tracks (admin) <Button sx={{ marginBottom: 2 }}>Create</Button></Heading>
|
||||||
|
<Box className="songCategory">
|
||||||
|
{
|
||||||
|
tracks.map(x => {
|
||||||
|
return <Song data={x}>
|
||||||
|
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary" onClick={async () => {
|
||||||
|
const Res = await axios.post("/api/library/me/bookmark", { SongID: x.ID });
|
||||||
|
if (Res.status === 200)
|
||||||
|
{
|
||||||
|
toast("Success!");
|
||||||
|
navigate("/profile");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
toast(Res.data.errorMessage, { type: "error" })
|
||||||
|
}}>Add to Bookmarks</Button>
|
||||||
|
<Button sx={{ width: "100%", marginBottom: 1 }}>View Details</Button>
|
||||||
|
<Button sx={{ width: "100%" }} variant="danger">Disable</Button>
|
||||||
|
</Song>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -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 { Divider } from "@primer/react/lib-esm/ActionList/Divider";
|
||||||
import { PageHeader } from "@primer/react/drafts";
|
import { PageHeader } from "@primer/react/drafts";
|
||||||
import { useContext } 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 { useNavigate } from "react-router-dom";
|
||||||
import { Song } from "../components/Song";
|
import { Song } from "../components/Song";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const { state, setState } = useContext(SiteContext);
|
const { state, setState } = useContext(SiteContext);
|
||||||
const [, , removeCookie] = useCookies();
|
const [, , removeCookie] = useCookies();
|
||||||
const navigate = useNavigate();
|
const [isActivateDialogOpen, setIsActivateDialogOpen] = useState<boolean>(false);
|
||||||
|
const [librarySongs, setLibrarySongs] = useState<unknown[]>([]);
|
||||||
|
const [bookmarkedSongs, setBookmarkedSongs] = useState<unknown[]>([]);
|
||||||
|
const [availableOverrides, setAvailableOverrides] = useState<{ Name: string, Template: string }[]>([]);
|
||||||
|
const [overriding, setOverriding] = useState<unknown>({});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<>
|
(async () => {
|
||||||
{
|
const Data = await axios.get("/api/library/me");
|
||||||
state.UserDetails ?
|
const Overrides = await axios.get("/api/library/available");
|
||||||
<Box sx={{ marginLeft: "15%", marginRight: "15%" }}>
|
|
||||||
<PageHeader>
|
if (Data.status !== 200 || Overrides.status !== 200)
|
||||||
<PageHeader.TitleArea variant="large">
|
return toast("An error has occured while getting your library!", { type: "error" });
|
||||||
<PageHeader.LeadingVisual>
|
|
||||||
<Avatar src={state.UserDetails.Avatar} size={32} alt={`${state.UserDetails.GlobalName} (@${state.UserDetails.Username})`} />
|
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 } });
|
||||||
</PageHeader.LeadingVisual>
|
const BookSongs = (await Promise.all(Data.data.Bookmarks.map((x: { ID: string; }) => axios.get(`/api/library/song/data/${x.ID}`)))).map(x => x.data);
|
||||||
<PageHeader.Title>
|
setLibrarySongs(LibSongs);
|
||||||
{state.UserDetails.GlobalName} (@{state.UserDetails.Username})
|
setBookmarkedSongs(BookSongs);
|
||||||
</PageHeader.Title>
|
setAvailableOverrides(Overrides.data);
|
||||||
<PageHeader.Actions>
|
})();
|
||||||
<Button size="large" variant="danger" onClick={() => { removeCookie("UserDetails"); removeCookie("Token"); setState({ ...state, UserDetails: null }); navigate("/"); }}>Log out</Button>
|
}, []);
|
||||||
</PageHeader.Actions>
|
|
||||||
</PageHeader.TitleArea>
|
return (
|
||||||
</PageHeader>
|
<>
|
||||||
<Divider />
|
{
|
||||||
<Heading sx={{ marginBottom: 2 }}>Active Songs</Heading>
|
state.UserDetails ?
|
||||||
<Box className="songCategory">
|
<Box>
|
||||||
<Song>
|
<PageHeader>
|
||||||
<center>Overriding: <Text sx={{ display: "block", fontWeight: "700", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>Party Rock Anthem</Text></center>
|
<PageHeader.TitleArea variant="large">
|
||||||
<Button sx={{ width: "100%", marginTop: 2 }} variant="danger">Remove from Active</Button>
|
<PageHeader.LeadingVisual>
|
||||||
</Song>
|
<Avatar src={state.UserDetails.Avatar} size={32} alt={`${state.UserDetails.GlobalName} (@${state.UserDetails.Username})`} />
|
||||||
</Box>
|
</PageHeader.LeadingVisual>
|
||||||
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>My Bookmarks</Heading>
|
<PageHeader.Title>
|
||||||
<Box className="songCategory">
|
{state.UserDetails.GlobalName} (@{state.UserDetails.Username})
|
||||||
<Song>
|
</PageHeader.Title>
|
||||||
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary">Add to Active</Button>
|
<PageHeader.Actions>
|
||||||
<Button sx={{ width: "100%" }} variant="danger">Remove from Bookmarks</Button>
|
<Button size="large" variant="danger" onClick={() => { removeCookie("UserDetails"); removeCookie("Token"); setState({ ...state, UserDetails: null }); navigate("/"); }}>Log out</Button>
|
||||||
</Song>
|
</PageHeader.Actions>
|
||||||
<Song>
|
</PageHeader.TitleArea>
|
||||||
<Button sx={{ width: "100%", marginBottom: 1 }} disabled>Add to Active</Button>
|
</PageHeader>
|
||||||
<Button sx={{ width: "100%" }} variant="danger">Remove from Bookmarks</Button>
|
<Divider />
|
||||||
</Song>
|
<Heading sx={{ marginBottom: 2 }}>Active Songs</Heading>
|
||||||
</Box>
|
<Dialog isOpen={isActivateDialogOpen} onDismiss={() => setIsActivateDialogOpen(false)} aria-labelledby="header">
|
||||||
</Box> :
|
<Dialog.Header id="header">Activate song</Dialog.Header>
|
||||||
<>
|
<Box p={3}>
|
||||||
<Text>You are not logged in.<br />Log in using the button in the top right.</Text>
|
<Text fontFamily="sans-serif">In order to activate a song for use, you need to sacrifice another song.<br />Please select a song you own to replace:</Text>
|
||||||
</>
|
<ActionMenu>
|
||||||
}
|
<ActionMenu.Button>Select a song...</ActionMenu.Button>
|
||||||
</>
|
<ActionMenu.Overlay width="medium">
|
||||||
)
|
<ActionList>
|
||||||
|
{
|
||||||
|
availableOverrides.map(x => {
|
||||||
|
return <ActionList.Item onSelect={async () => {
|
||||||
|
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}
|
||||||
|
</ActionList.Item>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ActionList>
|
||||||
|
</ActionMenu.Overlay>
|
||||||
|
</ActionMenu>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
<Box className="songCategory">
|
||||||
|
{
|
||||||
|
librarySongs.length >= 1 ?
|
||||||
|
librarySongs.map(x => {
|
||||||
|
return <Song data={x}>
|
||||||
|
<center>Overriding: <Text sx={{ display: "block", fontWeight: "700", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{availableOverrides.find(y => y.Template.toLowerCase() === x.Override.toLowerCase())?.Name}</Text></center>
|
||||||
|
<Button sx={{ width: "100%", marginTop: 2 }} variant="danger" onClick={async () => {
|
||||||
|
const Res = await axios.post("/api/library/me/deactivate", { SongID: x.ID });
|
||||||
|
if (Res.status === 200) {
|
||||||
|
librarySongs.splice(librarySongs.findIndex(y => y.ID === x.ID), 1);
|
||||||
|
setLibrarySongs([...librarySongs]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
toast(Res.data.errorMessage, { type: "error" })
|
||||||
|
}}>Remove from Active</Button>
|
||||||
|
</Song>;
|
||||||
|
})
|
||||||
|
: <Text>You have no activated songs.</Text>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>My Bookmarks</Heading>
|
||||||
|
<Box className="songCategory">
|
||||||
|
{
|
||||||
|
bookmarkedSongs.length >= 1 ?
|
||||||
|
bookmarkedSongs.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%" }} variant="danger" onClick={async () => {
|
||||||
|
const Res = await axios.post("/api/library/me/unbookmark", { SongID: x.ID });
|
||||||
|
if (Res.status === 200) {
|
||||||
|
bookmarkedSongs.splice(bookmarkedSongs.findIndex(y => y.ID === x.ID), 1);
|
||||||
|
setBookmarkedSongs([...bookmarkedSongs]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
toast(Res.data.errorMessage, { type: "error" })
|
||||||
|
}}>Remove from Bookmarks</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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
79
src/routes/Tracks.tsx
Normal file
79
src/routes/Tracks.tsx
Normal file
|
@ -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<unknown[]>([]);
|
||||||
|
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 <Box>
|
||||||
|
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>{x.Header}</Heading>
|
||||||
|
<Box className="songCategory">
|
||||||
|
{
|
||||||
|
x.Songs.map(x => {
|
||||||
|
return <Song data={x}>
|
||||||
|
{
|
||||||
|
bookmarks.findIndex(y => y.ID === x.ID) !== -1 ?
|
||||||
|
<Button sx={{ width: "100%", marginBottom: 1 }} variant="danger" onClick={async () => {
|
||||||
|
const Res = await axios.post("/api/library/me/unbookmark", { SongID: x.ID });
|
||||||
|
if (Res.status === 200)
|
||||||
|
{
|
||||||
|
bookmarks.splice(bookmarks.findIndex(y => y.ID === x.ID), 1);
|
||||||
|
setBookmarks([...bookmarks]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
toast(Res.data.errorMessage, { type: "error" })
|
||||||
|
}}>Remove from Bookmarks</Button> :
|
||||||
|
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary" onClick={async () => {
|
||||||
|
if (!state.UserDetails)
|
||||||
|
{
|
||||||
|
if (!state.DiscordOauthURL)
|
||||||
|
return toast("You are not logged in. Please log in first!");
|
||||||
|
|
||||||
|
return window.location.assign(state.DiscordOauthURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Res = await axios.post("/api/library/me/bookmark", { SongID: x.ID });
|
||||||
|
if (Res.status === 200)
|
||||||
|
{
|
||||||
|
bookmarks.push(x);
|
||||||
|
setBookmarks([...bookmarks]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
toast(Res.data.errorMessage, { type: "error" })
|
||||||
|
}}>Add to Bookmarks</Button>
|
||||||
|
}
|
||||||
|
</Song>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,7 +6,8 @@ export const SiteContext = createContext<IContext>({} as IContext);
|
||||||
export interface UserDetailInterface { ID: string, Username: string, GlobalName: string, Avatar: string }
|
export interface UserDetailInterface { ID: string, Username: string, GlobalName: string, Avatar: string }
|
||||||
|
|
||||||
export interface SiteState {
|
export interface SiteState {
|
||||||
UserDetails: UserDetailInterface | null
|
UserDetails: UserDetailInterface | null;
|
||||||
|
DiscordOauthURL: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IContext {
|
export interface IContext {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
|
|
Loading…
Reference in New Issue
Block a user