commit 6f8fd23f4906b7961af21c96359db1accd049610 Author: Wynd Date: Fri Aug 1 20:28:32 2025 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59c15a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +out/ +.hurl/ diff --git a/curse-stats.py b/curse-stats.py new file mode 100755 index 0000000..d5dd59e --- /dev/null +++ b/curse-stats.py @@ -0,0 +1,190 @@ +#!/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 = data["id"] + self.name = data["name"] + self.downloads = data["downloadCount"] + self.date_created = 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 = 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 = [] + for filter_list in args.filter: + for filter in filter_list: + filters.append(filter) + + sorts = [] + 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 + # pagination = data['pagination'] + 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)