mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-05 21:38:06 +03:00
feat: custom playlister + ui
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { ToAPIURL } from "util/api";
|
||||
import { CopyToClipboard } from "util/misc";
|
||||
import Card from "components/layout/page/Card";
|
||||
import ApiError from "components/util/ApiError";
|
||||
import Loading from "components/util/Loading";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import { TachiConfig } from "lib/config";
|
||||
import React from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { UGPT } from "types/react";
|
||||
import { GPTTool } from "types/ugpt";
|
||||
import Icon from "components/util/Icon";
|
||||
import Divider from "components/util/Divider";
|
||||
|
||||
function Component({ game, playtype, reqUser }: UGPT) {
|
||||
const { data, error } = useApiQuery<
|
||||
Array<{
|
||||
urlName: string;
|
||||
forSpecificUser?: boolean;
|
||||
playlistName: string;
|
||||
description: string;
|
||||
}>
|
||||
>(`/games/${game}/${playtype}/playlists`);
|
||||
|
||||
if (error) {
|
||||
return <ApiError error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<Card
|
||||
header="Supported Playlists"
|
||||
footer={
|
||||
<div className="w-100 d-flex justify-content-center align-items-center">
|
||||
<div>
|
||||
Just drop this file into your <code>playlists/</code> folder.
|
||||
</div>
|
||||
<a
|
||||
className="btn btn-outline-primary ml-4"
|
||||
download={`tachi-playlists.json`}
|
||||
href={`data:text/plain,${encodeURIComponent(
|
||||
JSON.stringify(
|
||||
data.map((e) =>
|
||||
ToAPIURL(
|
||||
`${
|
||||
e.forSpecificUser
|
||||
? `/users/${reqUser.username}`
|
||||
: ""
|
||||
}/games/iidx/${playtype}/playlists/${e.urlName}`
|
||||
)
|
||||
)
|
||||
)
|
||||
)}`}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ul>
|
||||
{data.map((e) => (
|
||||
<li key={e.urlName}>
|
||||
{e.playlistName} <br />
|
||||
{e.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export const IIDXPlaylistsTool: GPTTool = {
|
||||
name: `${TachiConfig.name} IIDX Playlists`,
|
||||
urlPath: "custom-tables",
|
||||
description: `${TachiConfig.name} has its own IIDX playlists that you can use in-game via playlister!`,
|
||||
component: Component,
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { Game, GetIDString, IDStrings, Playtype } from "tachi-common";
|
||||
import { GPTTool } from "types/ugpt";
|
||||
import { BMSCustomTablesTool } from "./BMSCustomTablesTool";
|
||||
import { IIDXEamusementExportTool } from "./IIDXEamusementExportTool";
|
||||
import { IIDXPlaylistsTool } from "./IIDXPlaylistsTool";
|
||||
|
||||
// A tool is something that users can use, like exporting to a specific format
|
||||
// or having things integrate with the game in a neat way.
|
||||
@@ -15,8 +16,8 @@ const GPT_TOOLS: Record<IDStrings, Array<GPTTool>> = {
|
||||
"chunithm:Single": [],
|
||||
"gitadora:Dora": [],
|
||||
"gitadora:Gita": [],
|
||||
"iidx:DP": [IIDXEamusementExportTool],
|
||||
"iidx:SP": [IIDXEamusementExportTool],
|
||||
"iidx:DP": [IIDXEamusementExportTool, IIDXPlaylistsTool],
|
||||
"iidx:SP": [IIDXEamusementExportTool, IIDXPlaylistsTool],
|
||||
"itg:Stamina": [],
|
||||
"jubeat:Single": [],
|
||||
"museca:Single": [],
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { PoyashiBPI } from "rg-stats";
|
||||
import type { integer, ChartDocument, Playtypes } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
interface PlaylistEntry {
|
||||
entry_id: integer;
|
||||
difficulty: "another" | "beginner" | "hyper" | "leggendaria" | "normal";
|
||||
bar_style?: "event" | "lightning" | "rainbow";
|
||||
}
|
||||
|
||||
export interface IIDXPlaylist {
|
||||
name: string;
|
||||
play_style: "DP" | "SP";
|
||||
charts: Array<PlaylistEntry>;
|
||||
}
|
||||
|
||||
export type TachiIIDXPlaylist = {
|
||||
playtype: Playtypes["iidx"] | null; // What playtype is this for? If null, this playlist is for any.
|
||||
|
||||
urlName: string; // what should we call this playlist in the url?
|
||||
playlistName: string; // what should this playlist be called in-game?
|
||||
description: string;
|
||||
} & (
|
||||
| {
|
||||
forSpecificUser: true; // playlist is user dependent.
|
||||
// like, say, their rivals scores or something.
|
||||
// then the callbacks need to recieve that info.
|
||||
getPlaylists: (
|
||||
userID: integer,
|
||||
playtype: Playtypes["iidx"]
|
||||
) => Promise<Array<IIDXPlaylist>>;
|
||||
}
|
||||
| {
|
||||
forSpecificUser?: false;
|
||||
getPlaylists: (playtype: Playtypes["iidx"]) => Promise<Array<IIDXPlaylist>>;
|
||||
}
|
||||
);
|
||||
|
||||
function ChartsToPlaylistFormat(
|
||||
charts: Array<ChartDocument<"iidx:DP" | "iidx:SP">>
|
||||
): Array<PlaylistEntry> {
|
||||
const arr: Array<PlaylistEntry> = [];
|
||||
|
||||
for (const chart of charts) {
|
||||
let inGameID: number;
|
||||
|
||||
// welp. pick the latest one.
|
||||
|
||||
if (Array.isArray(chart.data.inGameID)) {
|
||||
const el = chart.data.inGameID.at(-1);
|
||||
|
||||
if (el === undefined) {
|
||||
// wut, chart has an empty array of ingameids?
|
||||
logger.warn(`Chart '${chart.chartID}' has an empty array of inGameIDs.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
inGameID = el;
|
||||
} else {
|
||||
inGameID = chart.data.inGameID;
|
||||
}
|
||||
|
||||
// skip 2dxtra, this excludes things like "All Scratch ANOTHER".
|
||||
if (chart.data["2dxtraSet"] !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
arr.push({
|
||||
entry_id: inGameID,
|
||||
|
||||
// @ts-expect-error yeah, i know. it'll be fine.
|
||||
difficulty: chart.difficulty.toLowerCase(),
|
||||
|
||||
// highlight individual difference charts as "event".
|
||||
bar_style:
|
||||
chart.tierlistInfo["kt-HC"]?.individualDifference === true ? "event" : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const CUSTOM_TACHI_IIDX_PLAYLISTS: Array<TachiIIDXPlaylist> = [
|
||||
{
|
||||
urlName: "aaa-bpi",
|
||||
playtype: null,
|
||||
description: "Folders for how much an AAA is worth in BPI.",
|
||||
playlistName: "AAA BPIs",
|
||||
async getPlaylists(playtype) {
|
||||
const cutoffs = [-10, -5, 0, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
||||
|
||||
const bounds: Array<[number, number]> = [];
|
||||
|
||||
for (let i = 0; i < cutoffs.length - 1; i++) {
|
||||
bounds.push([cutoffs[i], cutoffs[i + 1]] as [number, number]);
|
||||
}
|
||||
|
||||
const charts = (await db.charts.iidx.find({
|
||||
"data.kaidenAverage": { $ne: null },
|
||||
})) as Array<ChartDocument<"iidx:DP" | "iidx:SP">>;
|
||||
|
||||
const entries = [];
|
||||
|
||||
for (const chart of charts) {
|
||||
// what's the least EX Score you can have for an AAA on this chart?
|
||||
const AAA_EX = Math.ceil(chart.data.notecount * 2 * (8 / 9));
|
||||
|
||||
entries.push({
|
||||
chart,
|
||||
bpi: PoyashiBPI.calculate(
|
||||
AAA_EX,
|
||||
chart.data.kaidenAverage!,
|
||||
chart.data.worldRecord!,
|
||||
chart.data.notecount * 2,
|
||||
chart.data.bpiCoefficient
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const playlists: Array<IIDXPlaylist> = [];
|
||||
|
||||
for (const [lower, upper] of bounds) {
|
||||
playlists.push({
|
||||
name: `AAA BPI ${lower}~${upper}`,
|
||||
play_style: playtype,
|
||||
charts: ChartsToPlaylistFormat(
|
||||
entries.filter((e) => e.bpi <= upper && e.bpi > lower).map((e) => e.chart)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return playlists;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Router } from "express";
|
||||
import { CUSTOM_TACHI_IIDX_PLAYLISTS } from "lib/game-specific/iidx-playlists";
|
||||
import type { TachiIIDXPlaylist } from "lib/game-specific/iidx-playlists";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
/**
|
||||
* List all the playlists we have available.
|
||||
*
|
||||
* @name GET /api/v1/games/iidx/:playtype/playlists
|
||||
*/
|
||||
|
||||
router.get("/:playtype/playlists", (req, res) => {
|
||||
const playlists = CUSTOM_TACHI_IIDX_PLAYLISTS.filter(
|
||||
(e) => e.playtype === null || e.playtype === req.params.playtype
|
||||
);
|
||||
|
||||
const body = [];
|
||||
|
||||
for (const playlist of playlists) {
|
||||
body.push({
|
||||
forSpecificUser: playlist.forSpecificUser,
|
||||
urlName: playlist.urlName,
|
||||
playlistName: playlist.playlistName,
|
||||
description: playlist.description,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Found ${playlists.length} playlist(s)`,
|
||||
body: playlists,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve this playlist.
|
||||
*
|
||||
* @name GET /api/v1/games/iidx/:playtype/playlists/:playlistID
|
||||
*/
|
||||
router.get("/:playtype/playlists/:playlistID", async (req, res) => {
|
||||
const playlist: TachiIIDXPlaylist | undefined = CUSTOM_TACHI_IIDX_PLAYLISTS.find(
|
||||
(e) =>
|
||||
(e.playtype === null || e.playtype === req.params.playtype) &&
|
||||
e.urlName === req.params.playlistID
|
||||
);
|
||||
|
||||
if (!playlist) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `No such playlist '${req.params.playlistID}' exists for '${req.params.playtype}'.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (playlist.forSpecificUser === true) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `This playlist is for a specific user. Use the /users/:userID endpoint instead.`,
|
||||
});
|
||||
}
|
||||
|
||||
const body = await playlist.getPlaylists(req.params.playtype as "DP" | "SP");
|
||||
|
||||
return res.status(200).json(body);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -2,10 +2,12 @@
|
||||
// and playtypes. This is for things like - say - custom BMS tables.
|
||||
|
||||
import bmsRouter from "./bms/router";
|
||||
import iidxRouter from "./iidx/router";
|
||||
import { Router } from "express";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.use("/bms", bmsRouter);
|
||||
router.use("/iidx", iidxRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
+36
-4
@@ -1,11 +1,10 @@
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { CUSTOM_TACHI_IIDX_PLAYLISTS } from "lib/game-specific/iidx-playlists";
|
||||
import { EAM_VERSION_NAMES } from "lib/score-import/import-types/common/eamusement-iidx-csv/parser";
|
||||
import {
|
||||
AggressiveRateLimitMiddleware,
|
||||
HyperAggressiveRateLimitMiddleware,
|
||||
} from "server/middleware/rate-limiter";
|
||||
import { AggressiveRateLimitMiddleware } from "server/middleware/rate-limiter";
|
||||
import { GetUser } from "utils/req-tachi-data";
|
||||
import type { TachiIIDXPlaylist } from "lib/game-specific/iidx-playlists";
|
||||
import type {
|
||||
ChartDocument,
|
||||
Grades,
|
||||
@@ -185,4 +184,37 @@ router.get("/:playtype/eamusement-csv", AggressiveRateLimitMiddleware, async (re
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve this playlist.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/iidx/:playtype/playlists/:playlistID
|
||||
*/
|
||||
router.get("/:playtype/playlists/:playlistID", async (req, res) => {
|
||||
const user = GetUser(req);
|
||||
|
||||
const playlist: TachiIIDXPlaylist | undefined = CUSTOM_TACHI_IIDX_PLAYLISTS.find(
|
||||
(e) =>
|
||||
(e.playtype === null || e.playtype === req.params.playtype) &&
|
||||
e.urlName === req.params.playlistID
|
||||
);
|
||||
|
||||
if (!playlist) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `No such playlist '${req.params.playlistID}' exists for '${req.params.playtype}'.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (playlist.forSpecificUser !== true) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `This playlist is not for a specific user. Use the /games/:game endpoint instead.`,
|
||||
});
|
||||
}
|
||||
|
||||
const body = await playlist.getPlaylists(user.id, req.params.playtype as "DP" | "SP");
|
||||
|
||||
return res.status(200).json(body);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user