Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cf support #5

Merged
merged 25 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c5206e5
Groundwork refactoring to support cf
TheBossMagnus Jan 26, 2024
776352f
refactors
TheBossMagnus Jan 26, 2024
35eae1a
detect modpack type
TheBossMagnus Jan 30, 2024
a6e3246
Split the get modpack data between mr and cf
TheBossMagnus Jan 31, 2024
085388c
fix wrongly formatted crash message
TheBossMagnus Jan 31, 2024
e5868be
fix wip funct args
TheBossMagnus Jan 31, 2024
167aec8
add test pack for cf
TheBossMagnus Jan 31, 2024
c66b937
fix cf having a different filename
TheBossMagnus Jan 31, 2024
044e390
basic cf modpack parsing support
TheBossMagnus Jan 31, 2024
e57e19b
Refactor cf modpack parsing
TheBossMagnus Feb 1, 2024
fcdd310
unused import
TheBossMagnus Feb 1, 2024
c482e3d
fix comment
TheBossMagnus Feb 1, 2024
9d5d21c
Merge branch 'master' into cf-support
TheBossMagnus Feb 1, 2024
d929600
Unfinshed not working cf requests
TheBossMagnus Feb 2, 2024
9a95d63
fix ssl
TheBossMagnus Feb 2, 2024
ec99a5c
fix added mod wrong detections
TheBossMagnus Feb 4, 2024
3f0b295
Merge branch 'cf-support' of https://github.com/TheBossMagnus/Modpack…
TheBossMagnus Feb 4, 2024
e2c868f
Finish draft of cf support
TheBossMagnus Feb 11, 2024
a175e82
Fix mr requests having cf headers
TheBossMagnus Feb 11, 2024
a632944
split the 2 request sistems
TheBossMagnus Feb 11, 2024
e64848e
Use a map to slect the correct function
TheBossMagnus Feb 11, 2024
a83a413
refactors
TheBossMagnus Feb 11, 2024
8ffeb18
Specify witch server gave the error
TheBossMagnus Feb 11, 2024
0490bb3
Merge the error handling for the 2 apis
TheBossMagnus Feb 14, 2024
0191787
Move arcoded url parts in constants.py
TheBossMagnus Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/ModpackChangelogger.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import argparse
import logging
import constants
from compare_packs import compare_packs
from config_handler import load_config
from constants import VERSION
from extract_pack_data import mr_get_pack_data, cf_get_pack_data
from get_json import get_json
from out import markdown_out

Expand All @@ -17,7 +18,7 @@ def setup_logging(debug):
# Clear the log file
with open('log.txt', 'w', encoding="utf-8") as f:
f.write('')

file_handler = logging.FileHandler('log.txt', encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%H:%M'))
Expand All @@ -26,7 +27,7 @@ def setup_logging(debug):
logging.basicConfig(level=logging.INFO, handlers=[console_handler])

logger = logging.getLogger(__name__)
logger.debug("Version: %s", VERSION)
logger.debug("Version: %s", constants.VERSION)


def parse_arguments():
Expand Down Expand Up @@ -56,13 +57,20 @@ def main(old_path, new_path, config_path, changelog_file, debug=False):
old_json = get_json(old_path)
new_json = get_json(new_path)

if constants.Modpacks_Format == 'modrinth':
old_ids, new_ids, old_info, new_info = mr_get_pack_data(old_json, new_json)
else:
old_ids, new_ids, old_info, new_info = cf_get_pack_data(old_json, new_json)

# Compare the packs
added, removed, updated = compare_packs(old_json, new_json, config)
added, removed, updated = compare_packs(old_ids, new_ids, old_info, new_info, config)
logger.debug("Added mods: %s\nRemoved mods:%s\nUpdated mods:%s\n", added, removed, updated)

# Print in a md doc
markdown_out(added, removed, updated, config, changelog_file)

if __name__ == "__main__":
args = parse_arguments()
if args.version:
print(f"ModpackChangelogger {VERSION}")
print(f"ModpackChangelogger {constants.VERSION}")
main(args.old, args.new, args.config, args.file, args.debug)
52 changes: 12 additions & 40 deletions src/compare_packs.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,25 @@
import asyncio
import logging
import re
from get_mod_names import get_mod_names

PATTERN = re.compile(r"(?<=data\/)[a-zA-Z0-9]{8}")

def get_dependency_info(json):
loader = next((key for key in json['dependencies'].keys() if key != 'minecraft'), "Unknown")
return {
'mc_version': json['dependencies']['minecraft'],
'loader': loader,
'loader_version': json['dependencies'][loader]
}

def get_mod_urls(json):
return [download for url in json['files'] for download in url['downloads']]

def extract_mod_ids(url_list):
return [PATTERN.search(str(url)).group(0) for url in url_list]

def compare_packs(old_json, new_json, config):
old_info, new_info = get_dependency_info(old_json), get_dependency_info(new_json)

new_urls, old_urls = set(get_mod_urls(new_json)), set(get_mod_urls(old_json))
new_urls, old_urls = new_urls.difference(old_urls), old_urls.difference(new_urls)

added_ids, removed_ids = set(extract_mod_ids(list(new_urls))), set(extract_mod_ids(list(old_urls)))
updated_ids = added_ids & removed_ids
added_ids -= updated_ids
removed_ids -= updated_ids
def compare_packs(old_ids, new_ids, old_info, new_info, config):
updated_ids = old_ids & new_ids
added_ids = new_ids - old_ids
removed_ids = old_ids - new_ids

# Remove a category if disabled in config
if not config['check']['added_mods']:
added_ids = set()
if not config['check']['removed_mods']:
removed_ids = set()
if not config['check']['updated_mods']:
updated_ids = set()
added_ids = added_ids if config['check']['added_mods'] else set()
removed_ids = removed_ids if config['check']['removed_mods'] else set()
updated_ids = updated_ids if config['check']['updated_mods'] else set()

added_mods, removed_mods, updated_mods = asyncio.run(get_mod_names(added_ids, removed_ids, updated_ids))

added_mods = sorted(mod for mod in added_mods if mod is not None)
removed_mods = sorted(mod for mod in removed_mods if mod is not None)
updated_mods = sorted(mod for mod in updated_mods if mod is not None)
added_mods = sorted(mod for mod in added_mods if mod)
removed_mods = sorted(mod for mod in removed_mods if mod)
updated_mods = sorted(mod for mod in updated_mods if mod)

if config['check']['loader']:
if old_info['loader'] != new_info['loader']:
if old_info['loader'] != new_info['loader']:
added_mods.append(f"{new_info['loader']} (mod loader)")
removed_mods.append(f"{old_info['loader']} (mod loader)")
logging.debug("Loader change detected: %s, new loader: %s", old_info['loader'], new_info['loader'])
Expand All @@ -57,6 +31,4 @@ def compare_packs(old_json, new_json, config):
updated_mods.append(f"Minecraft version {new_info['mc_version']}")
logging.debug("Minecraft version change detected: %s, new version: %s", old_info['mc_version'], new_info['mc_version'])

logging.debug("Added mods: %a\nRemoved mods: %a\nUpdated mods: %a", added_mods, removed_mods, updated_mods)

return added_mods, removed_mods, updated_mods
return added_mods, removed_mods, updated_mods
17 changes: 15 additions & 2 deletions src/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# This file contains some hardcoded values, do not edit it directly if you don't know what you are doing.

# Mod ecosistem used (modrinth or curseforge)
Modpacks_Format = None
# Version number
VERSION = "0.2.0"
# Default config
Expand All @@ -16,5 +18,16 @@
},
}
# Networking
HEADERS = {'User-Agent':f"TheBossMagnus/ModpackChangelogger/{VERSION} ([email protected])"}
MODRINTH_API_URL = "https://api.modrinth.com/v2"
MR_HEADERS = {'User-Agent':f"TheBossMagnus/ModpackChangelogger/{VERSION} ([email protected])"}
MR_API_URL = "https://api.modrinth.com/v2/projects?ids="

# DO NOT USE THIS KEY FOR YOUR OWN PROJECT/FORKS
CF_KEY = '$2a$10$GiT8VjJE8VJpcK68Wlz6aeJ5CPAZcRuTBcGuys8XtX5hGC87sIgku'
# You can get your own key at https://docs.curseforge.com

CF_HEADERS = {
'x-api-key': CF_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
CF_API_URL = "https://api.curseforge.com/v1/mods"
52 changes: 52 additions & 0 deletions src/extract_pack_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import re

def mr_get_pack_data(old_json, new_json):
PATTERN = re.compile(r"(?<=data\/)[a-zA-Z0-9]{8}")

def get_dependency_info(json):
loader = next((key for key in json['dependencies'].keys() if key != 'minecraft'), "Unknown")
return {
'mc_version': json['dependencies']['minecraft'],
'loader': loader,
'loader_version': json['dependencies'][loader]
}

def get_mod_urls(json):
return [download for url in json['files'] for download in url['downloads']]

def extract_mod_ids(url_list):
return [PATTERN.search(str(url)).group(0) for url in url_list]

old_info, new_info = get_dependency_info(old_json), get_dependency_info(new_json)
new_urls, old_urls = set(get_mod_urls(new_json)), set(get_mod_urls(old_json))
# remove urls that are in both packs (not added nor removed nor updated)
common_urls = new_urls & old_urls
new_urls -= common_urls
old_urls -= common_urls

old_ids, new_ids = set(extract_mod_ids(new_urls)), set(extract_mod_ids(old_urls))
return old_ids, new_ids, old_info, new_info

def cf_get_pack_data(old_json, new_json):
def get_dependency_info(json):
loader_string = json['minecraft']['modLoaders'][0]['id']
return {
'mc_version': json['minecraft']['version'],
'loader': loader_string.split('-')[0],
'loader_version': loader_string.split('-')[1]
}

def get_mod_ids(json):
# Extracts the file and project IDs from the JSON
return {item['fileID']: item['projectID'] for item in json['files']}

old_file_ids = get_mod_ids(old_json)
new_file_ids = get_mod_ids(new_json)

# To reference a file cf has a project id (the mod) and a file id (the version)
# with this code we can get mod that have been changed between the two packs, so with unquie file ids
old_ids = {old_file_ids[file_id] for file_id in old_file_ids if file_id not in new_file_ids}
new_ids = {new_file_ids[file_id] for file_id in new_file_ids if file_id not in old_file_ids}

old_info, new_info = get_dependency_info(old_json), get_dependency_info(new_json)
return old_ids, new_ids, old_info, new_info
24 changes: 18 additions & 6 deletions src/get_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@
import shutil
import sys
from zipfile import ZipFile

import constants

def get_json(path):
# Ensure the file is a modpack file
if not path.endswith('.mrpack'):
logging.error('ERROR: Input file is not a modpack')
sys.exit(1)
if not os.path.exists(path):
logging.error('ERROR: The file %s does not exist', path)
sys.exit(1)

if path.endswith('.mrpack'):
if constants.Modpacks_Format == 'curseforge':
logging.error('ERROR: Using Modrinth and a Curseforge modpack together is not supported')
sys.exit(1)
constants.Modpacks_Format = 'modrinth'
logging.debug('Using Modrinth format modpack')
elif path.endswith('.zip'):
if constants.Modpacks_Format == 'modrinth':
logging.error('ERROR: Using Modrinth and a Curseforge modpack together is not supported')
sys.exit(1)
logging.debug('Using CurseForge format modpack')
constants.Modpacks_Format = 'curseforge'
else:
logging.error('ERROR: Input modpack is not in a supported format')
sys.exit(1)

# Create a temporary directory
temp_dir = os.path.join(os.environ.get('TEMP'), 'ModpackChangelogger')
os.makedirs(temp_dir, exist_ok=True)
Expand All @@ -26,7 +38,7 @@ def get_json(path):
logging.debug('Extracted %s to %s', path, temp_dir)

# Parse the json file
json_path = os.path.join(temp_dir, 'modrinth.index.json')
json_path = os.path.join(temp_dir, 'modrinth.index.json' if constants.Modpacks_Format == 'modrinth' else 'manifest.json')
with open(json_path, 'r', encoding="utf-8") as json_file:
logging.debug('Parsed %s', json_path)
return json.load(json_file)
Expand Down
58 changes: 39 additions & 19 deletions src/get_mod_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,57 @@
import json
import logging
import aiohttp
from constants import MODRINTH_API_URL, HEADERS
from constants import MR_API_URL, MR_HEADERS, CF_HEADERS, CF_API_URL
import constants

async def get_mod_names(added_ids, removed_ids, updated_ids):
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as session:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=0.1)) as session:
api_function = {
"modrinth": request_from_mr_api,
"curseforge": request_from_cf_api
}.get(constants.Modpacks_Format, request_from_cf_api)

added_names, removed_names, updated_names = await asyncio.gather(
request_from_api(session, added_ids),
request_from_api(session, removed_ids),
request_from_api(session, updated_ids)
api_function(session, added_ids),
api_function(session, removed_ids),
api_function(session, updated_ids)
)

return added_names, removed_names, updated_names

async def request_from_api(session, ids):
# Convert the set of ids to a json array
ids_list = json.dumps(list(ids))
URL = f"{MODRINTH_API_URL}/projects?ids={ids_list}"
async def request_from_mr_api(session, ids):
names = []
URL = f"{MR_API_URL}{json.dumps(list(ids))}"

try:
async with session.get(URL, headers=HEADERS) as response:
async with session.get(URL, headers=MR_HEADERS) as response:
response.raise_for_status()
data = await response.json()
names = [project.get('title') for project in data]
except aiohttp.ClientConnectionError as e:
except (aiohttp.ClientConnectionError, asyncio.TimeoutError, aiohttp.ClientResponseError) as e:
handle_request_errors(e, URL)

return names

async def request_from_cf_api(session, ids):
names = []
URL = f"{CF_API_URL}"

try:
async with session.post(URL, headers=CF_HEADERS, json={'modIds': list(ids)}, ssl=False) as response:
response = await response.json()
names = [project['name'] for project in response['data']]
except (aiohttp.ClientConnectionError, asyncio.TimeoutError, aiohttp.ClientResponseError) as e:
handle_request_errors(e, URL)

return names

def handle_request_errors(e, URL):
if isinstance(e, aiohttp.ClientConnectionError):
logging.warning("Failed to connect to %s: %s", URL, e)
except asyncio.TimeoutError:
logging.warning("The request %s timed out ", URL)
except aiohttp.ClientResponseError as e:
elif isinstance(e, asyncio.TimeoutError):
logging.warning("The request %s timed out", URL)
elif isinstance(e, aiohttp.ClientResponseError):
logging.warning("Server responded with an error for %s: %s", URL, e)
except aiohttp.ClientPayloadError as e:
logging.warning("Failed to read response from %s: %s", URL, e)
except aiohttp.ClientError as e:
else:
logging.warning("An unexpected error occurred: %s", e)

return names
Binary file added tests/new.zip
Binary file not shown.
Binary file added tests/old.zip
Binary file not shown.