#!/bin/python3 import requests import os import pathlib from enum import Enum from argparse import ArgumentParser from dotenv import load_dotenv from tabulate import tabulate from datetime import datetime, timedelta, timezone class Mod: def __init__(self, data): self.id = int(data["id"]) self.name = str(data["name"]) self.downloads = int(data["downloadCount"]) self.date_created = str(data["dateCreated"]) year_created = "" try: year_created = datetime.strptime( data["dateCreated"], "%Y-%m-%dT%H:%M:%S.%f%z" ).strftime("%Y") except ValueError: year_created = datetime.strptime( data["dateCreated"], "%Y-%m-%dT%H:%M:%S%z" ).strftime("%Y") self.year_created = int(year_created) self.date_modified = str(data["dateModified"]) try: self.date_released = datetime.strptime( data["dateReleased"], "%Y-%m-%dT%H:%M:%S.%f%z" ) except ValueError: self.date_released = datetime.strptime( data["dateReleased"], "%Y-%m-%dT%H:%M:%S%z" ) class FilterType(Enum): LastYear = "last-year" Downloads = "downloads" class SortType(Enum): Downloads = "downloads" InvDownloads = "-downloads" ReleaseYear = "release-year" InvReleaseYear = "-release-year" class ModLoaderType(Enum): Any = 0 Forge = 1 Liteloader = 3 Fabric = 4 Quilt = 5 NeoForge = 6 if __name__ == "__main__": load_dotenv() def clamp(val: int, smallest: int, largest: int) -> int: return max(smallest, min(val, largest)) pathlib.Path("./out").mkdir(exist_ok=True) parser = ArgumentParser() parser.add_argument( "-f", "--filter", help="Filters what mods are included", type=FilterType, required=False, action="append", nargs="+", dest="filter", ) parser.add_argument( "-s", "--sort", help="Sorts the mods before putting them in a table", type=SortType, required=False, action="append", nargs="+", dest="sort", ) parser.add_argument( "--page-size", help="Entries per page", type=int, default=50, required=False, dest="page_size", ) parser.add_argument( "--max-index", help="Max amount of entries fetched", type=int, default=10_000, required=False, dest="max_index", ) parser.add_argument( "--count", help="Fetched only the max number of entries of the search", default=False, required=False, action="store_true", dest="count", ) parser.add_argument( "--game-version", help="Narrows down the search for only a specific game version", type=str, default="", required=False, dest="game_version", ) parser.add_argument( "--mod-loader", help="Narrows down the search for only a specific mod loader", type=str, default="any", required=False, dest="mod_loader", ) args = parser.parse_args() page_size = clamp(args.page_size, 10, 50) # Merge all filters into 1 array regardless of the arg format filters = [] if args.filter is not None: for filter_list in args.filter: for filter in filter_list: filters.append(filter) sorts = [] if args.sort is not None: for sort_list in args.sort: for sort_logic in sort_list: sorts.append(sort_logic) def sort_mod(mod: Mod, sorts: list[SortType]): sorting = [] for sort in sorts: if sort is SortType.Downloads: sorting.append(mod.downloads) elif sort is SortType.InvDownloads: sorting.append(-mod.downloads) elif sort is SortType.ReleaseYear: sorting.append(mod.year_created) elif sort is SortType.InvReleaseYear: sorting.append(-mod.year_created) return tuple(sorting) MOD_LOADER = ModLoaderType[args.mod_loader] GAME_VERSION = args.game_version ONLY_COUNT = args.count PAGE_SIZE = page_size MAX_INDEX = args.max_index URL = "https://api.curseforge.com/v1/mods/search" PARAMS = { "gameId": "432", # minecraft "classId": 6, # mods "gameVersion": GAME_VERSION, "modLoaderType": MOD_LOADER.value, "sortField": 6, # downloads "sortOrder": "desc", "pageSize": PAGE_SIZE, "index": 0, } CURSE_API_TOKEN = os.environ["CURSE_API_TOKEN"] HEADERS = {"x-api-key": CURSE_API_TOKEN} all_mods = [] count = 0 for i in range(0, MAX_INDEX, PAGE_SIZE): PARAMS["index"] = i r = requests.get(url=URL, params=PARAMS, headers=HEADERS) try: data = r.json() except ValueError as err: print(r) print(err) break if ONLY_COUNT: count = data["pagination"]["totalCount"] break data = data["data"] for entry in data: mod = Mod(entry) # filter mods with under 1M downloads if FilterType.Downloads in filters: if mod.downloads < 1_000_000: continue # filter mods without a release in the past year if FilterType.LastYear in filters: past = datetime.now(timezone.utc) - timedelta(days=365) if mod.date_released < past: continue all_mods.append(mod) print(f"{i} / {MAX_INDEX}") if ONLY_COUNT: print(f"{MOD_LOADER.name}-{GAME_VERSION}: {count}") exit(0) all_mods.sort(key=lambda m: sort_mod(m, sorts)) all_mods = map( lambda m: [ m.name, "{0:,d}".format(m.downloads), m.date_created, m.date_released, ], all_mods, ) table = tabulate( all_mods, headers=["Name", "Downloads", "Created Date", "Last Release Date"], tablefmt="double_grid", showindex=True, ) # Write the above table in a file for logging with today's date as the file's name with open("./out/mods", "w") as f: f.write(table)