mirror of
https://github.com/Radiquum/YAMPD.git
synced 2025-05-20 15:49: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 = os.environ.copy()
|
||||||
environment["is_dev"] = "True"
|
environment["is_dev"] = "True"
|
||||||
environment["NEXT_PUBLIC_API_URL"] = "http://127.0.0.1:5000/api"
|
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: handle multiple package managers line npm(node), deno, yarn
|
||||||
# TODO?: install node deps automatically
|
# TODO?: install node deps automatically
|
||||||
|
|
|
@ -12,6 +12,11 @@ type _PACKS_ENDPOINT = {
|
||||||
deletePack: string;
|
deletePack: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type _MOD_ENDPOINT = {
|
||||||
|
addMod: string;
|
||||||
|
deleteMod: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const PACK_ENDPOINT = (endpoint: keyof _PACK_ENDPOINT, id: string) => {
|
export const PACK_ENDPOINT = (endpoint: keyof _PACK_ENDPOINT, id: string) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
console.error(`ENDPOINT "${endpoint}" REQUIRES A PACK ID`);
|
console.error(`ENDPOINT "${endpoint}" REQUIRES A PACK ID`);
|
||||||
|
@ -43,3 +48,26 @@ export const PACKS_ENDPOINT = (
|
||||||
};
|
};
|
||||||
return _endpoints[endpoint];
|
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);
|
const ur = new URL(window.location.href);
|
||||||
ur.searchParams.set("id", data.id);
|
ur.searchParams.set("id", data.id);
|
||||||
ur.pathname = "/pack";
|
ur.pathname = "/pack";
|
||||||
|
|
|
@ -1,26 +1,55 @@
|
||||||
"use client";
|
"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 { 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 { useSearchParams } from "next/navigation";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
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 { ModTable } from "../components/ModTable";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { Mod } from "@/types/mod";
|
||||||
|
|
||||||
export default function PackPage() {
|
export default function PackPage() {
|
||||||
const [packData, setPackData] = useState<Pack | null>(null);
|
const [packData, setPackData] = useState<Pack | null>(null);
|
||||||
|
const [packMods, setPackMods] = useState<Mod[]>([]);
|
||||||
const [packDataLoading, setPackDataLoading] = useState(true);
|
const [packDataLoading, setPackDataLoading] = useState(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const id = useSearchParams().get("id") || "";
|
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(() => {
|
useEffect(() => {
|
||||||
async function _getPacksData() {
|
async function _getPacksData() {
|
||||||
const res = await fetch(PACK_ENDPOINT("getPack", id));
|
const res = await fetch(PACK_ENDPOINT("getPack", id));
|
||||||
if (!res.ok) router.push("/404");
|
if (!res.ok) router.push("/404");
|
||||||
setPackData(await res.json());
|
const data: Pack = await res.json();
|
||||||
|
setPackData(data);
|
||||||
|
setPackMods(data.mods);
|
||||||
setPackDataLoading(false);
|
setPackDataLoading(false);
|
||||||
}
|
}
|
||||||
if (id) {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{packDataLoading && (
|
{packDataLoading && (
|
||||||
|
@ -104,12 +254,15 @@ export default function PackPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button color={"red"} onClick={() => deletePack()}>
|
<Button onClick={() => setAddModModalOpen(true)}>
|
||||||
Delete <HiTrash className="ml-2 h-5 w-5" />
|
Add mod <HiPlusCircle className="ml-2 h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button>
|
<Button>
|
||||||
Download <HiDownload className="ml-2 h-5 w-5" />
|
Download <HiDownload className="ml-2 h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button color={"red"} onClick={() => deletePack()}>
|
||||||
|
Delete <HiTrash className="ml-2 h-5 w-5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -118,6 +271,55 @@ export default function PackPage() {
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,13 @@ from PIL import Image
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
from .source import Modrinth
|
||||||
|
|
||||||
|
|
||||||
@apiPack.route("/<id>", methods=["GET"])
|
@apiPack.route("/<id>", methods=["GET"])
|
||||||
def getPack(id):
|
def getPack(id):
|
||||||
if not os.path.exists(f"{PACKS_FOLDER}/{id}/packfile.json"):
|
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 = {}
|
pack = {}
|
||||||
with open(f"{PACKS_FOLDER}/{id}/packfile.json") as fp:
|
with open(f"{PACKS_FOLDER}/{id}/packfile.json") as fp:
|
||||||
|
@ -59,3 +60,49 @@ def editPackImage(id):
|
||||||
"message": "image updated",
|
"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"
|
PACKS_FOLDER = "../packs"
|
||||||
|
|
||||||
IMG_ALLOWED_MIME = {"image/png", "image/jpg", "image/jpeg", "image/webp", "image/jfif"}
|
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