#!/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 = 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" 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", ) 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(-int(mod.year_created)) return tuple(sorting) PAGE_SIZE = page_size MAX_INDEX = args.max_index URL = "https://api.curseforge.com/v1/mods/search" PARAMS = { "gameId": "432", # minecraft "classId": 6, # mods "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 = [] 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(err) 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}") 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)