284 lines
8.5 KiB
Python
Executable File
284 lines
8.5 KiB
Python
Executable File
#!/usr/bin/env nix-shell
|
|
#!nix-shell -i python3 -p python3Packages.requests python3Packages.dataclasses-json
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import requests
|
|
from dataclasses_json import DataClassJsonMixin, LetterCase, config
|
|
from marshmallow import fields
|
|
|
|
|
|
@dataclass
|
|
class Download(DataClassJsonMixin):
|
|
sha1: str
|
|
size: int
|
|
url: str
|
|
|
|
|
|
@dataclass
|
|
class Version(DataClassJsonMixin):
|
|
id: str
|
|
type: str
|
|
url: str
|
|
time: datetime = field(
|
|
metadata=config(
|
|
encoder=datetime.isoformat,
|
|
decoder=datetime.fromisoformat,
|
|
mm_field=fields.DateTime(format="iso"),
|
|
)
|
|
)
|
|
release_time: datetime = field(
|
|
metadata=config(
|
|
encoder=datetime.isoformat,
|
|
decoder=datetime.fromisoformat,
|
|
mm_field=fields.DateTime(format="iso"),
|
|
letter_case=LetterCase.CAMEL,
|
|
)
|
|
)
|
|
|
|
def get_manifest(self) -> Any:
|
|
"""Return the version's manifest."""
|
|
response = requests.get(self.url)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
def get_downloads(self) -> Dict[str, Download]:
|
|
"""
|
|
Return all downloadable files from the version's manifest, in Download
|
|
objects.
|
|
"""
|
|
return {
|
|
download_name: Download.from_dict(download_info)
|
|
for download_name, download_info in self.get_manifest()["downloads"].items()
|
|
}
|
|
|
|
def get_java_version(self) -> Any:
|
|
"""
|
|
Return the java version specified in a version's manifest, if it is
|
|
present. Versions <= 1.6 do not specify this.
|
|
"""
|
|
return self.get_manifest().get("javaVersion", {}).get("majorVersion", None)
|
|
|
|
def get_server(self) -> Optional[Download]:
|
|
"""
|
|
If the version has a server download available, return the Download
|
|
object for the server download. If the version does not have a server
|
|
download avilable, return None.
|
|
"""
|
|
downloads = self.get_downloads()
|
|
if "server" in downloads:
|
|
return downloads["server"]
|
|
return None
|
|
|
|
|
|
def get_versions() -> List[Version]:
|
|
"""Return a list of Version objects for all available versions."""
|
|
response = requests.get(
|
|
"https://launchermeta.mojang.com/mc/game/version_manifest.json"
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
return [Version.from_dict(version) for version in data["versions"]]
|
|
|
|
|
|
def get_major_release(version_id: str) -> str:
|
|
"""
|
|
Return the major release for a version. The major release for 1.17 and
|
|
1.17.1 is 1.17.
|
|
"""
|
|
if not len(version_id.split(".")) >= 2:
|
|
raise ValueError(f"version not in expected format: '{version_id}'")
|
|
return ".".join(version_id.split(".")[:2])
|
|
|
|
|
|
def group_major_releases(releases: List[Version]) -> Dict[str, List[Version]]:
|
|
"""
|
|
Return a dictionary containing each version grouped by each major release.
|
|
The key "1.17" contains a list with two Version objects, one for "1.17"
|
|
and another for "1.17.1".
|
|
"""
|
|
groups: Dict[str, List[Version]] = {}
|
|
for release in releases:
|
|
major_release = get_major_release(release.id)
|
|
if major_release not in groups:
|
|
groups[major_release] = []
|
|
groups[major_release].append(release)
|
|
return groups
|
|
|
|
|
|
def slugify(version: str) -> str:
|
|
return version.replace(".", "-")
|
|
|
|
|
|
def get_changelog_url(version: str) -> Optional[str]:
|
|
"""
|
|
Attempt to resolve the Minecraft changelog article URL.
|
|
Returns the URL if it exists, otherwise None.
|
|
"""
|
|
url = f"https://www.minecraft.net/en-us/article/minecraft-java-edition-{slugify(version)}"
|
|
|
|
# our request is denied without a human user-agent
|
|
headers = {
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0"
|
|
}
|
|
|
|
try:
|
|
response = requests.head(url, headers=headers, timeout=3)
|
|
if response.status_code == 200:
|
|
return url
|
|
except requests.RequestException as e:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
def get_latest_major_releases(releases: List[Version]) -> Dict[str, Version]:
|
|
"""
|
|
Return a dictionary containing the latest version for each major release.
|
|
The latest major release for 1.16 is 1.16.5, so the key "1.16" contains a
|
|
Version object for 1.16.5.
|
|
"""
|
|
return {
|
|
major_release: max(
|
|
(
|
|
release
|
|
for release in releases
|
|
if get_major_release(release.id) == major_release
|
|
),
|
|
key=lambda x: tuple(map(int, x.id.split("."))),
|
|
)
|
|
for major_release in group_major_releases(releases)
|
|
}
|
|
|
|
|
|
def generate() -> Dict[str, Dict[str, str]]:
|
|
"""
|
|
Return a dictionary containing the latest url, sha1 and version for each major
|
|
release.
|
|
"""
|
|
versions = get_versions()
|
|
releases = list(
|
|
filter(lambda version: version.type == "release", versions)
|
|
) # remove snapshots and betas
|
|
latest_major_releases = get_latest_major_releases(releases)
|
|
|
|
servers = {
|
|
version: Download.schema().dump(download_info) # Download -> dict
|
|
for version, download_info in {
|
|
version: value.get_server()
|
|
for version, value in latest_major_releases.items()
|
|
}.items()
|
|
if download_info is not None # versions < 1.2 do not have a server
|
|
}
|
|
for server in servers.values():
|
|
del server["size"] # don't need it
|
|
|
|
for version, server in servers.items():
|
|
server["version"] = latest_major_releases[version].id
|
|
server["javaVersion"] = latest_major_releases[version].get_java_version()
|
|
return servers
|
|
|
|
|
|
def get_latest(servers: Dict[str, Dict[str, str]]) -> str | None:
|
|
return max(
|
|
(v.get("version") for v in servers.values()),
|
|
key=lambda x: tuple(map(int, x.split("."))) if x is not None else (),
|
|
)
|
|
|
|
|
|
def generate_commit(
|
|
previous_servers: Dict[str, Dict[str, str]],
|
|
servers: Dict[str, Dict[str, str]],
|
|
versions_file: Path,
|
|
) -> List[Dict[str, str | list[str]]]:
|
|
actions = []
|
|
commit_body_lines = []
|
|
|
|
old_latest = get_latest(previous_servers)
|
|
new_latest = get_latest(servers)
|
|
|
|
for major_version, server in servers.items():
|
|
version = server.get("version")
|
|
previous_server = previous_servers.get(major_version)
|
|
|
|
if version is None:
|
|
continue
|
|
|
|
attribute = f"minecraftServers.vanilla-{slugify(major_version)}"
|
|
|
|
if not previous_server:
|
|
# this version didn't exist before
|
|
# check if its now the latest version
|
|
if version == new_latest:
|
|
action = f"{old_latest} -> {new_latest}"
|
|
attribute = "minecraft-server"
|
|
else:
|
|
action = f"init {version}"
|
|
|
|
else:
|
|
previous_version = previous_server.get("version")
|
|
if previous_version == version:
|
|
continue
|
|
|
|
action = f"{previous_version} -> {version}"
|
|
|
|
actions.append(action)
|
|
|
|
commit_body_lines.append(f"{attribute}: {action}")
|
|
|
|
changelog_url = get_changelog_url(version)
|
|
if changelog_url:
|
|
commit_body_lines.append(f"Release notes: {changelog_url}")
|
|
|
|
if not commit_body_lines:
|
|
return []
|
|
|
|
if len(actions) == 1:
|
|
commit_message = commit_body_lines[0]
|
|
|
|
# the body should only be the release notes to avoid repeatition
|
|
# if the release notes don't exist this will be blank
|
|
commit_body = "\n".join(commit_body_lines[1:]).strip()
|
|
else:
|
|
detailed_message = f"minecraft-server: {', '.join(actions)}"
|
|
|
|
commit_message = (
|
|
detailed_message
|
|
if len(detailed_message) <= 72
|
|
else "minecraft-server: update multiple versions"
|
|
)
|
|
|
|
commit_body = "\n".join(commit_body_lines).strip()
|
|
|
|
commit_json = {
|
|
"attrPath": "minecraftServers.vanilla",
|
|
"files": [str(versions_file)],
|
|
"commitMessage": commit_message,
|
|
}
|
|
|
|
if commit_body:
|
|
commit_json["commitBody"] = commit_body
|
|
|
|
return [commit_json]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
versions_file = Path(__file__).parent / "versions.json"
|
|
|
|
with open(versions_file, "r") as file:
|
|
previous_servers = json.load(file)
|
|
|
|
servers = generate()
|
|
|
|
commit_json = generate_commit(previous_servers, servers, versions_file)
|
|
|
|
with open(versions_file, "w") as file:
|
|
json.dump(servers, file, indent=2)
|
|
file.write("\n")
|
|
|
|
print(json.dumps(commit_json))
|