diff --git a/.gitignore b/.gitignore index 59c15a7..56e5569 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env out/ .hurl/ +__pycache__ diff --git a/curse-stats.py b/curse-stats.py index a19280a..fbccd49 100755 --- a/curse-stats.py +++ b/curse-stats.py @@ -11,8 +11,9 @@ from datetime import datetime, timedelta, timezone class Mod: - def __init__(self, data): - self.id = int(data["id"]) + def from_curse(self, data): + self.id = str(data["id"]) + self.platform = "CURSE" self.name = str(data["name"]) self.downloads = int(data["downloadCount"]) self.date_created = str(data["dateCreated"]) @@ -36,6 +37,33 @@ class Mod: data["dateReleased"], "%Y-%m-%dT%H:%M:%S%z" ) + return self + + def from_modrinth(self, data): + self.id = str(data["project_id"]) + self.platform = "MODRINTH" + self.name = str(data["title"]) + self.downloads = int(data["downloads"]) + self.date_created = str(data["date_created"]) + self.date_released = datetime.strptime( + self.date_created, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + year_created = "" + try: + year_created = datetime.strptime( + data["date_created"], "%Y-%m-%dT%H:%M:%S.%f%z" + ).strftime("%Y") + except ValueError: + year_created = datetime.strptime( + data["date_created"], "%Y-%m-%dT%H:%M:%S%z" + ).strftime("%Y") + self.year_created = int(year_created) + self.date_modified = str(data["date_modified"]) + + return self + + # def __init__(self, data): + class FilterType(Enum): LastYear = "last-year" @@ -58,6 +86,115 @@ class ModLoaderType(Enum): NeoForge = 6 +def filter_mod(mod: Mod) -> bool: + # filter mods with under 1M downloads + if FilterType.Downloads in filters: + if mod.downloads < 1_000_000: + return False + + # 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: + return False + + return True + + +def fetch_curse_mods() -> tuple[list[Mod], int]: + 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} + + count = 0 + mods: list[Mod] = [] + + 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 = int(data["pagination"]["totalCount"]) + break + + data = data["data"] + + for entry in data: + mod = Mod().from_curse(entry) + if filter_mod(mod): + mods.append(mod) + + print(f"{i} / {MAX_INDEX}") + + return (mods, count) + + +def fetch_modrinth_mods() -> tuple[list[Mod], int]: + URL = "https://api.modrinth.com/v2/search" + PARAMS = { + "facets": format( + f'[["project_type:mod"],["categories:{MOD_LOADER.name}"],["versions:{GAME_VERSION}"]]' + ), + "limit": PAGE_SIZE, + "index": "downloads", + } + HEADERS = {} + + count = 0 + mods: list[Mod] = [] + + for i in range(0, MAX_INDEX, PAGE_SIZE): + 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["total_hits"] + break + + data = data["hits"] + + for entry in data: + mod = Mod().from_modrinth(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 + + mods.append(mod) + + print(f"{i} / {MAX_INDEX}") + + return (mods, count) + + if __name__ == "__main__": load_dotenv() @@ -144,7 +281,7 @@ if __name__ == "__main__": for sort_logic in sort_list: sorts.append(sort_logic) - def sort_mod(mod: Mod, sorts: list[SortType]): + def sort_mod(mod: Mod, sorts: list[SortType]) -> list: sorting = [] for sort in sorts: if sort is SortType.Downloads: @@ -156,72 +293,35 @@ if __name__ == "__main__": elif sort is SortType.InvReleaseYear: sorting.append(-mod.year_created) - return tuple(sorting) + return 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 + all_mods: list[Mod] = [] - 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 + (curse_mods, curse_count) = fetch_curse_mods() + all_mods.extend(curse_mods) - 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}") + (modrinth_mods, modrinth_count) = fetch_modrinth_mods() + all_mods.extend(modrinth_mods) if ONLY_COUNT: - print(f"{MOD_LOADER.name}-{GAME_VERSION}: {count}") + print(f"{MOD_LOADER.name}-{GAME_VERSION} {curse_count + modrinth_count}") + # print( + # f"{MOD_LOADER.name}-{GAME_VERSION}\n Curse {curse_count}\n Modrinth {modrinth_count}" + # ) exit(0) all_mods.sort(key=lambda m: sort_mod(m, sorts)) - all_mods = map( + mods_map = map( lambda m: [ m.name, + m.platform, "{0:,d}".format(m.downloads), m.date_created, m.date_released, @@ -229,8 +329,8 @@ if __name__ == "__main__": all_mods, ) table = tabulate( - all_mods, - headers=["Name", "Downloads", "Created Date", "Last Release Date"], + mods_map, + headers=["Name", "Platform", "Downloads", "Created Date", "Last Release Date"], tablefmt="double_grid", showindex=True, ) diff --git a/justfile b/justfile index 2e69d2d..8f27375 100644 --- a/justfile +++ b/justfile @@ -5,6 +5,7 @@ count-120: #!/usr/bin/env bash for i in {1..6}; do ./curse-stats.py --game-version "1.20.${i}" --mod-loader Forge --count + printf "\n" done count-121: @@ -12,4 +13,5 @@ count-121: for i in {1..8}; do ./curse-stats.py --game-version "1.21.${i}" --mod-loader Forge --count ./curse-stats.py --game-version "1.21.${i}" --mod-loader NeoForge --count + printf "\n" done