minecraft_version.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import re
  2. import json
  3. import requests
  4. from enum import Enum
  5. from typing import Optional, TypedDict, Literal
  6. from logging import Logger
  7. # region 版本模式定义
  8. DEVELOP_VERSION_PATTERN = re.compile(r'^(?P<major>[\d.]+)-(?P<type>[a-zA-Z]+)-?(?P<index>\d+)$')
  9. "开发版本号模式"
  10. RELEASE_PATTERN = re.compile(r'^\d{1,2}\.\d+(\.\d+)?$')
  11. "正式版本号模式"
  12. OLD_SNAPSHOT_PATTERN = re.compile(r'^(1\d)|(2[0-5])[w|W]\d{2}[A-Fa-f]$')
  13. "旧版本快照模式,用于 1.21.11 及以前的版本"
  14. # endregion
  15. # region 常量定义
  16. OFFICIAL_CHANGELOG_URL_PREFIX = 'https://minecraft.net/article/minecraft'
  17. LAUNCHER_MANIFEST_URL = 'https://piston-meta.mojang.com/mc/game/version_manifest.json'
  18. # endregion
  19. # region 官网更新日志重载词典
  20. OFFICIAL_CHANGE_LOG_OVERRIDE_LINKS: dict[str, str] = {}
  21. # endregion
  22. logger = Logger('MCV')
  23. # region 版本信息类定义
  24. class MinecraftManifestVersionInfo(TypedDict):
  25. id: str
  26. type: Literal['snapshot', 'release', 'old_beta', 'old_alpha']
  27. url: str
  28. time: str
  29. releaseTime: str
  30. class MinecraftManifestLatestVersionInfo(TypedDict):
  31. snapshot: Optional[str]
  32. release: Optional[str]
  33. class MinecraftManifest(TypedDict):
  34. latest: MinecraftManifestLatestVersionInfo
  35. versions: list[MinecraftManifestVersionInfo]
  36. # endregion
  37. # region 版本类定义
  38. class VersionType(Enum):
  39. SNAPSHOT = 'snapshot'
  40. PRE_RELEASE = 'pre'
  41. RELEASE_CANDIDATE = 'rc'
  42. RELEASE = 'release'
  43. OTHER = 'other'
  44. class MinecraftVersion:
  45. id: str
  46. type: VersionType
  47. url: str
  48. time: str
  49. release_time: str
  50. use_old_id_format: bool = False
  51. major_version: Optional[str] = None
  52. release_index: Optional[str] = None
  53. __server_jar_url: Optional[str] = None
  54. @property
  55. def server_jar_url(self) -> str:
  56. if self.__server_jar_url:
  57. return self.__server_jar_url
  58. server_jar_url: Optional[str]
  59. response = requests.get(self.url, timeout=10000)
  60. server_jar_url = json.loads(response.content).get('downloads').get('server').get('url')
  61. if not server_jar_url:
  62. raise ValueError(f"版本 {self.id} 的服务器端下载链接不存在")
  63. self.__server_jar_url = server_jar_url
  64. return server_jar_url
  65. __change_log_url: Optional[str] = None
  66. @property
  67. def change_log_url(self) -> str:
  68. """获取版本更新日志链接"""
  69. if self.__change_log_url:
  70. return self.__change_log_url
  71. if override_url := OFFICIAL_CHANGE_LOG_OVERRIDE_LINKS.get(self.id):
  72. self.__change_log_url = override_url
  73. return override_url
  74. if self.type == VersionType.OTHER:
  75. return ""
  76. if self.use_old_id_format:
  77. self.__change_log_url = OFFICIAL_CHANGELOG_URL_PREFIX + '-snapshot-' + self.id.lower()
  78. elif self.type == VersionType.RELEASE:
  79. self.__change_log_url = f"{OFFICIAL_CHANGELOG_URL_PREFIX}-java-edition-{self.id.replace('.', '-')}"
  80. else:
  81. assert self.major_version is not None
  82. self.__change_log_url = f"{OFFICIAL_CHANGELOG_URL_PREFIX}-" \
  83. f"{self.major_version.replace('.', '-')}-" \
  84. f"{self.type.name.lower().replace('_', '-')}-{self.release_index}"
  85. return self.__change_log_url
  86. def __init__(self, version_id: str, url: str, time: str, release_time: str):
  87. self.id = version_id
  88. self.url = url
  89. self.time = time
  90. self.release_time = release_time
  91. if m := re.match(DEVELOP_VERSION_PATTERN, version_id):
  92. self.type = VersionType(m.group('type').lower())
  93. self.major_version = m.group('major')
  94. self.release_index = m.group('index')
  95. elif re.match(OLD_SNAPSHOT_PATTERN, version_id):
  96. self.type = VersionType.SNAPSHOT
  97. self.use_old_id_format = True
  98. elif re.match(RELEASE_PATTERN, version_id):
  99. self.type = VersionType.RELEASE
  100. self.major_version = version_id
  101. else:
  102. self.type = VersionType.OTHER
  103. class MinecraftVersionManager:
  104. __latest_development_version_id: Optional[str]
  105. __latest_release_version_id: Optional[str]
  106. __manifest: MinecraftManifest
  107. __manifest_versions: list[MinecraftManifestVersionInfo]
  108. __versions: dict[str, MinecraftVersion] = {}
  109. __version_ids: list[str] = []
  110. def __init__(self):
  111. self.refresh_manifest()
  112. self.__latest_development_version_id = self.__manifest['latest']['snapshot']
  113. self.__latest_release_version_id = self.__manifest['latest']['release']
  114. self.__manifest_versions = self.__manifest.get('versions', [])
  115. self.__version_ids = [version['id'] for version in self.__manifest_versions]
  116. def get(self, version_id: str) -> Optional[MinecraftVersion]:
  117. if version_id in self.__versions:
  118. return self.__versions[version_id]
  119. for version_info in self.__manifest_versions:
  120. if version_info['id'] == version_id:
  121. version = MinecraftVersion(version_id=version_info['id'],
  122. url=version_info['url'],
  123. time=version_info['time'],
  124. release_time=version_info['releaseTime'])
  125. self.__versions[version_id] = version
  126. return version
  127. return None
  128. def get_version_id_list(self) -> list[str]:
  129. return self.__version_ids
  130. def get_latest_version(self, version_type: Literal['any', 'development', 'release'] = 'any'
  131. ) -> Optional[MinecraftVersion]:
  132. if version_type == 'any':
  133. if self.__latest_development_version_id:
  134. return self.get(self.__latest_development_version_id)
  135. elif self.__latest_release_version_id:
  136. return self.get(self.__latest_release_version_id)
  137. else:
  138. return None
  139. elif version_type == 'development' and self.__latest_development_version_id:
  140. return self.get(self.__latest_development_version_id)
  141. elif version_type == 'release' and self.__latest_release_version_id:
  142. return self.get(self.__latest_release_version_id)
  143. else:
  144. return None
  145. @property
  146. def versions(self):
  147. for version_id in self.__version_ids:
  148. version = self.get(version_id)
  149. assert version is not None
  150. yield version
  151. def get_latest_development_versions(self) -> Optional[list[MinecraftVersion]]:
  152. latest_version = self.get_latest_version()
  153. if not latest_version or latest_version.type == VersionType.RELEASE:
  154. return None
  155. latest_development_versions = []
  156. current_major_version_branches = set()
  157. for version in self.versions:
  158. if version.major_version in current_major_version_branches:
  159. break
  160. if version.type == VersionType.RELEASE:
  161. break
  162. if version.type == VersionType.OTHER:
  163. continue
  164. current_major_version_branches.add(version.major_version)
  165. latest_development_versions.append(version)
  166. return latest_development_versions
  167. def refresh_manifest(self):
  168. self.__manifest = self.fetch_manifest()
  169. @classmethod
  170. def fetch_manifest(cls) -> MinecraftManifest:
  171. logger.info("正在加载 MC 版本列表")
  172. try:
  173. response = requests.get(LAUNCHER_MANIFEST_URL, timeout=10000)
  174. return json.loads(response.content)
  175. except Exception as e:
  176. logger.warning("加载 MC 版本列表失败")
  177. logger.exception(e)
  178. return MinecraftManifest(latest=MinecraftManifestLatestVersionInfo(snapshot=None, release=None),
  179. versions=[])
  180. # endregion
  181. MCVM = MinecraftVersionManager()