···1+#!/usr/bin/env nix-shell
2+#!nix-shell -i python3 -p python3Packages.requests python3Packages.dataclasses-json
3+4+import json
5+from dataclasses import dataclass, field
6+from datetime import datetime
7+from typing import Any, Dict, List, Optional
8+9+import requests
10+from dataclasses_json import DataClassJsonMixin, LetterCase, config
11+from marshmallow import fields
12+13+14+@dataclass
15+class Download(DataClassJsonMixin):
16+ sha1: str
17+ size: int
18+ url: str
19+20+21+@dataclass
22+class Version(DataClassJsonMixin):
23+ id: str
24+ type: str
25+ url: str
26+ time: datetime = field(
27+ metadata=config(
28+ encoder=datetime.isoformat,
29+ decoder=datetime.fromisoformat,
30+ mm_field=fields.DateTime(format="iso"),
31+ )
32+ )
33+ release_time: datetime = field(
34+ metadata=config(
35+ encoder=datetime.isoformat,
36+ decoder=datetime.fromisoformat,
37+ mm_field=fields.DateTime(format="iso"),
38+ letter_case=LetterCase.CAMEL,
39+ )
40+ )
41+42+ def get_manifest(self) -> Any:
43+ """Return the version's manifest."""
44+ response = requests.get(self.url)
45+ response.raise_for_status()
46+ return response.json()
47+48+ def get_downloads(self) -> Dict[str, Download]:
49+ """
50+ Return all downloadable files from the version's manifest, in Download
51+ objects.
52+ """
53+ return {
54+ download_name: Download.from_dict(download_info)
55+ for download_name, download_info in self.get_manifest()["downloads"].items()
56+ }
57+58+ def get_server(self) -> Optional[Download]:
59+ """
60+ If the version has a server download available, return the Download
61+ object for the server download. If the version does not have a server
62+ download avilable, return None.
63+ """
64+ downloads = self.get_downloads()
65+ if "server" in downloads:
66+ return downloads["server"]
67+ return None
68+69+70+def get_versions() -> List[Version]:
71+ """Return a list of Version objects for all available versions."""
72+ response = requests.get(
73+ "https://launchermeta.mojang.com/mc/game/version_manifest.json"
74+ )
75+ response.raise_for_status()
76+ data = response.json()
77+ return [Version.from_dict(version) for version in data["versions"]]
78+79+80+def get_major_release(version_id: str) -> str:
81+ """
82+ Return the major release for a version. The major release for 1.17 and
83+ 1.17.1 is 1.17.
84+ """
85+ if not len(version_id.split(".")) >= 2:
86+ raise ValueError(f"version not in expected format: '{version_id}'")
87+ return ".".join(version_id.split(".")[:2])
88+89+90+def group_major_releases(releases: List[Version]) -> Dict[str, List[Version]]:
91+ """
92+ Return a dictionary containing each version grouped by each major release.
93+ The key "1.17" contains a list with two Version objects, one for "1.17"
94+ and another for "1.17.1".
95+ """
96+ groups: Dict[str, List[Version]] = {}
97+ for release in releases:
98+ major_release = get_major_release(release.id)
99+ if major_release not in groups:
100+ groups[major_release] = []
101+ groups[major_release].append(release)
102+ return groups
103+104+105+def get_latest_major_releases(releases: List[Version]) -> Dict[str, Version]:
106+ """
107+ Return a dictionary containing the latest version for each major release.
108+ The latest major release for 1.16 is 1.16.5, so the key "1.16" contains a
109+ Version object for 1.16.5.
110+ """
111+ return {
112+ major_release: sorted(releases, key=lambda x: x.id, reverse=True)[0]
113+ for major_release, releases in group_major_releases(releases).items()
114+ }
115+116+117+def generate() -> Dict[str, Dict[str, str]]:
118+ """
119+ Return a dictionary containing the latest url, sha1 and version for each major
120+ release.
121+ """
122+ versions = get_versions()
123+ releases = list(
124+ filter(lambda version: version.type == "release", versions)
125+ ) # remove snapshots and betas
126+ latest_major_releases = get_latest_major_releases(releases)
127+128+ servers = {
129+ version: Download.schema().dump(download_info) # Download -> dict
130+ for version, download_info in {
131+ version: value.get_server()
132+ for version, value in latest_major_releases.items()
133+ }.items()
134+ if download_info is not None # versions < 1.2 do not have a server
135+ }
136+ for server in servers.values():
137+ del server["size"] # don't need it
138+139+ for version, server in servers.items():
140+ server["version"] = latest_major_releases[version].id
141+ return servers
142+143+144+if __name__ == "__main__":
145+ with open("versions.json", "w") as file:
146+ json.dump(generate(), file, indent=2)
147+ file.write("\n")