mirror of
https://github.com/Radiquum/YAMPD.git
synced 2025-05-19 23:29:34 +05:00
feat: add modrinth mod
This commit is contained in:
parent
237ce9879d
commit
e453f336a8
8 changed files with 398 additions and 21 deletions
2
dev.py
2
dev.py
|
@ -7,6 +7,8 @@ if __name__ == "__main__":
|
|||
environment = os.environ.copy()
|
||||
environment["is_dev"] = "True"
|
||||
environment["NEXT_PUBLIC_API_URL"] = "http://127.0.0.1:5000/api"
|
||||
environment["MODRINTH_UA"] = "radiquum/YAMPD (kentai.waah@gmail.com)"
|
||||
environment["CURSEFORGE_API_KEY"] = "$2a$10$bL4bIL5pUWqfcO7KQtnMReakwtfHbNKh6v1uTpKlzhwoueEJQnPnm"
|
||||
|
||||
# TODO: handle multiple package managers line npm(node), deno, yarn
|
||||
# TODO?: install node deps automatically
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
const API = process.env.NEXT_PUBLIC_API_URL || "/api";
|
||||
|
||||
type _PACK_ENDPOINT = {
|
||||
getPack: string;
|
||||
getPackImage: string;
|
||||
getPack: string;
|
||||
getPackImage: string;
|
||||
editPackImage: string;
|
||||
};
|
||||
|
||||
type _PACKS_ENDPOINT = {
|
||||
getPacks: string;
|
||||
getPacks: string;
|
||||
createPack: string;
|
||||
deletePack: string;
|
||||
};
|
||||
|
||||
type _MOD_ENDPOINT = {
|
||||
addMod: string;
|
||||
deleteMod: string;
|
||||
};
|
||||
|
||||
export const PACK_ENDPOINT = (endpoint: keyof _PACK_ENDPOINT, id: string) => {
|
||||
if (!id) {
|
||||
console.error(`ENDPOINT "${endpoint}" REQUIRES A PACK ID`);
|
||||
|
@ -19,8 +24,8 @@ export const PACK_ENDPOINT = (endpoint: keyof _PACK_ENDPOINT, id: string) => {
|
|||
}
|
||||
|
||||
const _endpoints = {
|
||||
getPack: `${API}/pack/${id}`,
|
||||
getPackImage: `${API}/pack/${id}/image`,
|
||||
getPack: `${API}/pack/${id}`,
|
||||
getPackImage: `${API}/pack/${id}/image`,
|
||||
editPackImage: `${API}/pack/${id}/image/edit`,
|
||||
};
|
||||
return _endpoints[endpoint];
|
||||
|
@ -37,9 +42,32 @@ export const PACKS_ENDPOINT = (
|
|||
}
|
||||
|
||||
const _endpoints = {
|
||||
getPacks: `${API}/packs/all`,
|
||||
getPacks: `${API}/packs/all`,
|
||||
createPack: `${API}/packs/new`,
|
||||
deletePack: `${API}/packs/${id}/delete`,
|
||||
};
|
||||
return _endpoints[endpoint];
|
||||
};
|
||||
|
||||
export const MOD_ENDPOINT = (
|
||||
endpoint: keyof _MOD_ENDPOINT,
|
||||
id: string,
|
||||
slug?: string | null
|
||||
) => {
|
||||
if (!id) {
|
||||
console.error(`ENDPOINT "${endpoint}" REQUIRES A PACK ID`);
|
||||
return "";
|
||||
}
|
||||
|
||||
const requireSlug: string[] = ["deleteMod"];
|
||||
if (requireSlug.includes(endpoint) && !slug) {
|
||||
console.error(`ENDPOINT "${endpoint}" REQUIRES A MOD SLUG`);
|
||||
return "";
|
||||
}
|
||||
|
||||
const _endpoints = {
|
||||
addMod: `${API}/pack/${id}/mod/add`,
|
||||
deleteMod: `${API}/pack/${id}/mod/${slug}/delete`,
|
||||
};
|
||||
return _endpoints[endpoint];
|
||||
};
|
||||
|
|
|
@ -86,14 +86,6 @@ export default function PackNew() {
|
|||
});
|
||||
}
|
||||
|
||||
toast.update(tid, {
|
||||
render: data.message,
|
||||
type: "success",
|
||||
isLoading: false,
|
||||
autoClose: 2500,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
const ur = new URL(window.location.href);
|
||||
ur.searchParams.set("id", data.id);
|
||||
ur.pathname = "/pack";
|
||||
|
|
|
@ -1,26 +1,55 @@
|
|||
"use client";
|
||||
|
||||
import { PACK_ENDPOINT, PACKS_ENDPOINT } from "@/api/ENDPOINTS";
|
||||
import { MOD_ENDPOINT, PACK_ENDPOINT, PACKS_ENDPOINT } from "@/api/ENDPOINTS";
|
||||
import { Pack } from "@/types/pack";
|
||||
import { Button, Card, Spinner } from "flowbite-react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Label,
|
||||
Select,
|
||||
Spinner,
|
||||
TextInput,
|
||||
} from "flowbite-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
|
||||
|
||||
import { HiDownload, HiTrash } from "react-icons/hi";
|
||||
import { HiDownload, HiPlusCircle, HiTrash } from "react-icons/hi";
|
||||
import { ModTable } from "../components/ModTable";
|
||||
import { toast } from "react-toastify";
|
||||
import { Mod } from "@/types/mod";
|
||||
|
||||
export default function PackPage() {
|
||||
const [packData, setPackData] = useState<Pack | null>(null);
|
||||
const [packMods, setPackMods] = useState<Mod[]>([]);
|
||||
const [packDataLoading, setPackDataLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
const id = useSearchParams().get("id") || "";
|
||||
|
||||
const [addModModalOpen, setAddModModalOpen] = useState(true);
|
||||
const [modSource, setModSource] = useState<"Modrinth" | "CurseForge">(
|
||||
"Modrinth"
|
||||
);
|
||||
const [modUrl, setModUrl] = useState({
|
||||
placeholer: "https://modrinth.com/mod/{ slug }</version/{ file_id }>",
|
||||
value: "",
|
||||
});
|
||||
// const [modParams, setModParams] = useState<{
|
||||
// slug: string;
|
||||
// version: string | null;
|
||||
// }>({
|
||||
// slug: "",
|
||||
// version: null,
|
||||
// });
|
||||
|
||||
useEffect(() => {
|
||||
async function _getPacksData() {
|
||||
const res = await fetch(PACK_ENDPOINT("getPack", id));
|
||||
if (!res.ok) router.push("/404");
|
||||
setPackData(await res.json());
|
||||
const data: Pack = await res.json();
|
||||
setPackData(data);
|
||||
setPackMods(data.mods);
|
||||
setPackDataLoading(false);
|
||||
}
|
||||
if (id) {
|
||||
|
@ -67,6 +96,127 @@ export default function PackPage() {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleModSource = (e: any) => {
|
||||
switch (e.target.value) {
|
||||
case "Modrinth":
|
||||
setModSource("Modrinth");
|
||||
setModUrl({
|
||||
placeholer: "https://modrinth.com/mod/{ slug }</version/{ file_id }>",
|
||||
value: "",
|
||||
});
|
||||
break;
|
||||
case "CurseForge":
|
||||
setModSource("CurseForge");
|
||||
setModUrl({
|
||||
placeholer:
|
||||
"https://www.curseforge.com/minecraft/mc-mods/{ slug }</files/{ file_id }>",
|
||||
value: "",
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
async function addMod() {
|
||||
let slug = null;
|
||||
let version = null;
|
||||
|
||||
if (!modUrl.value) {
|
||||
toast.error("Mod url is required", {
|
||||
autoClose: 2500,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (modSource) {
|
||||
case "Modrinth":
|
||||
const _tmp = modUrl.value.split("/mod/");
|
||||
if (_tmp.length == 1) {
|
||||
toast.error("invalid Modrinth url", {
|
||||
autoClose: 2500,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const _tmp2 = _tmp[1].split("/version/");
|
||||
slug = _tmp2[0];
|
||||
if (_tmp2.length > 1) {
|
||||
version = _tmp2[1];
|
||||
}
|
||||
break;
|
||||
case "CurseForge":
|
||||
const _tmp3 = modUrl.value.split("/mc-mods/");
|
||||
if (_tmp3.length == 1) {
|
||||
toast.error("invalid CurseForge url", {
|
||||
autoClose: 2500,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const _tmp4 = _tmp3[1].split("/files/");
|
||||
slug = _tmp4[0];
|
||||
if (_tmp4.length > 1) {
|
||||
version = _tmp4[1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
slug = slug.replace("/", "");
|
||||
version = version ? version.replace("/", "") : null;
|
||||
|
||||
// if (packMods.find((elem) => elem.slug == slug)) {
|
||||
// toast.error(`mod (${slug}) already exists`, {
|
||||
// autoClose: 2500,
|
||||
// closeOnClick: true,
|
||||
// draggable: true,
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!packData) return;
|
||||
const tid = toast.loading(`Adding mod`);
|
||||
const res = await fetch(MOD_ENDPOINT("addMod", packData._id), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
slug,
|
||||
version,
|
||||
source: modSource,
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status != "ok") {
|
||||
toast.update(tid, {
|
||||
render: data.message,
|
||||
type: "error",
|
||||
isLoading: false,
|
||||
autoClose: 2500,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setPackMods([...packMods, data.mod]);
|
||||
toast.update(tid, {
|
||||
render: data.message,
|
||||
type: "success",
|
||||
isLoading: false,
|
||||
autoClose: 2500,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
setModUrl({ ...modUrl, value: "" })
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{packDataLoading && (
|
||||
|
@ -104,12 +254,15 @@ export default function PackPage() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button color={"red"} onClick={() => deletePack()}>
|
||||
Delete <HiTrash className="ml-2 h-5 w-5" />
|
||||
<Button onClick={() => setAddModModalOpen(true)}>
|
||||
Add mod <HiPlusCircle className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
<Button>
|
||||
Download <HiDownload className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
<Button color={"red"} onClick={() => deletePack()}>
|
||||
Delete <HiTrash className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -118,6 +271,55 @@ export default function PackPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
dismissible
|
||||
show={addModModalOpen}
|
||||
onClose={() => setAddModModalOpen(false)}
|
||||
>
|
||||
<ModalHeader>Terms of Service</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="space-y-6">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 block">
|
||||
<Label htmlFor="base" className="text-lg">
|
||||
Source
|
||||
</Label>
|
||||
</div>
|
||||
<Select
|
||||
id="source"
|
||||
name="source"
|
||||
required
|
||||
onChange={(e) => handleModSource(e)}
|
||||
>
|
||||
<option value="Modrinth">Modrinth</option>
|
||||
<option value="CurseForge">CurseForge</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 block">
|
||||
<Label htmlFor="base" className="text-lg">
|
||||
Link
|
||||
</Label>
|
||||
</div>
|
||||
<TextInput
|
||||
id="base"
|
||||
type="text"
|
||||
sizing="md"
|
||||
name="author"
|
||||
onChange={(e) =>
|
||||
setModUrl({ ...modUrl, value: e.target.value })
|
||||
}
|
||||
value={modUrl.value}
|
||||
placeholder={modUrl.placeholer}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={() => addMod()}>Save</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,12 +7,13 @@ from PIL import Image
|
|||
from io import BytesIO
|
||||
import base64
|
||||
import json
|
||||
from .source import Modrinth
|
||||
|
||||
|
||||
@apiPack.route("/<id>", methods=["GET"])
|
||||
def getPack(id):
|
||||
if not os.path.exists(f"{PACKS_FOLDER}/{id}/packfile.json"):
|
||||
return jsonify({"status": "error", "message": "not found"}), 404
|
||||
return jsonify({"status": "error", "message": "pack not found"}), 404
|
||||
|
||||
pack = {}
|
||||
with open(f"{PACKS_FOLDER}/{id}/packfile.json") as fp:
|
||||
|
@ -59,3 +60,49 @@ def editPackImage(id):
|
|||
"message": "image updated",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@apiPack.route("/<id>/mod/add", methods=["POST"])
|
||||
def addMod(id):
|
||||
source = request.json.get("source", None)
|
||||
slug = request.json.get("slug", None)
|
||||
version = request.json.get("version", None)
|
||||
pack = {}
|
||||
mod = {}
|
||||
|
||||
if not os.path.exists(f"{PACKS_FOLDER}/{id}/packfile.json"):
|
||||
return jsonify({"status": "error", "message": "pack not found"})
|
||||
with open(f"{PACKS_FOLDER}/{id}/packfile.json") as fp:
|
||||
pack = json.load(fp)
|
||||
fp.close()
|
||||
mod_loader = pack.get("modloader").lower()
|
||||
game_version = pack.get("version")
|
||||
|
||||
if not source:
|
||||
return jsonify({"status": "error", "message": "mod source is required"})
|
||||
if not slug:
|
||||
return jsonify({"status": "error", "message": "mod slug is required"})
|
||||
|
||||
for mod in pack["mods"]:
|
||||
if mod.get("slug") == slug:
|
||||
return jsonify({"status": "error", "message": "mod already exists"})
|
||||
|
||||
if source == "Modrinth":
|
||||
mod = Modrinth.getModrinthMod(slug, version, mod_loader, game_version)
|
||||
|
||||
if mod.get("status") != "ok":
|
||||
return jsonify({"status": "error", "message": mod.get("message")})
|
||||
|
||||
pack["modpackVersion"] += 1
|
||||
pack["mods"].append(mod.get("mod"))
|
||||
with open(f"{PACKS_FOLDER}/{id}/packfile.json", mode="w", encoding="utf-8") as fp:
|
||||
json.dump(pack, fp)
|
||||
fp.close()
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"status": "ok",
|
||||
"message": f"mod {mod.get("mod").get('title')} ({slug}) has been added",
|
||||
"mod": mod.get("mod"),
|
||||
}
|
||||
)
|
||||
|
|
98
src/api/source/Modrinth.py
Normal file
98
src/api/source/Modrinth.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
import requests
|
||||
from config import MODRINTH_UA
|
||||
|
||||
|
||||
def getModrinthMod(slug, version, mod_loader, game_version):
|
||||
headers = {"User-Agent": MODRINTH_UA}
|
||||
descR = requests.get(f"https://api.modrinth.com/v2/project/{slug}", headers=headers)
|
||||
|
||||
if descR.status_code != 200:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"failed to fetch modrinth description: {descR.status_code}",
|
||||
}
|
||||
|
||||
versR = requests.get(
|
||||
f'https://api.modrinth.com/v2/project/{slug}/version?loaders=["{mod_loader}"]&game_versions=["{game_version}"]',
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if versR.status_code != 200:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"failed to fetch modrinth mod versions: {versR.status_code}",
|
||||
}
|
||||
|
||||
devsR = requests.get(
|
||||
f"https://api.modrinth.com/v2/project/{slug}/members",
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if devsR.status_code != 200:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"failed to fetch modrinth mod developers: {devsR.status_code}",
|
||||
}
|
||||
|
||||
desc: dict = descR.json()
|
||||
vers: dict = versR.json()
|
||||
devs: dict = devsR.json()
|
||||
|
||||
if len(vers) == 0:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"mod is not compatible with this game version or mod loader",
|
||||
}
|
||||
|
||||
selected_version = vers[0]
|
||||
if version:
|
||||
for _sf in vers:
|
||||
if _sf.get("version_number") == version:
|
||||
selected_version = _sf
|
||||
break
|
||||
|
||||
primary_file = None
|
||||
for _pf in selected_version.get("files"):
|
||||
if _pf.get("primary") == True:
|
||||
primary_file = _pf
|
||||
break
|
||||
|
||||
if primary_file is None:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"failed to get primary mod file",
|
||||
}
|
||||
|
||||
developers = []
|
||||
for dev in devs:
|
||||
developers.append(dev["user"]["username"])
|
||||
|
||||
isClient = False
|
||||
if desc.get("client_side") in ["optional", "required"]:
|
||||
isClient = True
|
||||
isServer = False
|
||||
if desc.get("server_side") in ["optional", "required"]:
|
||||
isServer = True
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"mod": {
|
||||
"slug": slug,
|
||||
"icon": desc.get("icon_url"),
|
||||
"title": desc.get("title"),
|
||||
"developers": developers,
|
||||
"source": "Modrinth",
|
||||
"url": f"https://modrinth.com/mod/{slug}",
|
||||
"environment": {
|
||||
"client": isClient,
|
||||
"server": isServer,
|
||||
},
|
||||
"file": {
|
||||
"version": selected_version.get("version_number"),
|
||||
"hashes": primary_file.get("hashes"),
|
||||
"url": primary_file.get("url"),
|
||||
"filename": primary_file.get("filename"),
|
||||
"size": primary_file.get("size"),
|
||||
},
|
||||
},
|
||||
}
|
0
src/api/source/__init__.py
Normal file
0
src/api/source/__init__.py
Normal file
|
@ -5,3 +5,11 @@ if os.getenv("is_dev") == "True":
|
|||
PACKS_FOLDER = "../packs"
|
||||
|
||||
IMG_ALLOWED_MIME = {"image/png", "image/jpg", "image/jpeg", "image/webp", "image/jfif"}
|
||||
|
||||
MODRINTH_UA = None
|
||||
if os.getenv("MODRINTH_UA"):
|
||||
MODRINTH_UA = os.getenv("MODRINTH_UA")
|
||||
|
||||
CURSEFORGE_API_KEY = None
|
||||
if os.getenv("CURSEFORGE_API_KEY"):
|
||||
CURSEFORGE_API_KEY = os.getenv("CURSEFORGE_API_KEY")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue