minecraft_versions.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import re
  2. import json
  3. import requests
  4. from enum import Enum
  5. from typing import Optional, TypedDict, Literal
  6. from homepagebuilder.core.logger import Logger
  7. from homepagebuilder.interfaces import script, format_code
  8. from homepagebuilder.core.types import Context
  9. from homepagebuilder.core.utils.event import listen_event as on
  10. # region 版本模式定义
  11. DEVELOP_VERSION_PATTERN = re.compile(r'^(?P<major>[\d.]+)-(?P<type>[a-zA-Z]+)-?(?P<index>\d+)$')
  12. "开发版本号模式"
  13. RELEASE_PATTERN = re.compile(r'^\d{1,2}\.\d+(\.\d+)?$')
  14. "正式版本号模式"
  15. OLD_SNAPSHOT_PATTERN = re.compile(r'^(1\d)|(2[0-5])[w|W]\d{2}[A-Fa-f]$')
  16. "旧版本快照模式,用于 1.21.11 及以前的版本"
  17. # endregion
  18. logger = Logger('MinecraftVersion')
  19. # region 常量定义
  20. OFFICIAL_CHANGELOG_URL_PREFIX = 'https://minecraft.net/article/minecraft'
  21. LAUNCHER_MANIFEST_URL = 'https://piston-meta.mojang.com/mc/game/version_manifest.json'
  22. # endregion
  23. # region 官网更新日志重载词典
  24. OFFICIAL_CHANGE_LOG_OVERRIDE_LINKS: dict[str, str] = {}
  25. @on('project.import.return')
  26. def setup_official_changelog_override_links(*_, **__) -> None:
  27. global OFFICIAL_CHANGE_LOG_OVERRIDE_LINKS, MCVM
  28. OFFICIAL_CHANGE_LOG_OVERRIDE_LINKS = Context.get_current_context().data.get('OfficialLink')
  29. # endregion
  30. # region 版本信息类定义
  31. class MinecraftManifestVersionInfo(TypedDict):
  32. id: str
  33. type: Literal['snapshot', 'release', 'old_beta', 'old_alpha']
  34. url: str
  35. time: str
  36. releaseTime: str
  37. class MinecraftManifestLatestVersionInfo(TypedDict):
  38. snapshot: Optional[str]
  39. release: Optional[str]
  40. class MinecraftManifest(TypedDict):
  41. latest: MinecraftManifestLatestVersionInfo
  42. versions: list[MinecraftManifestVersionInfo]
  43. # endregion
  44. # region 版本类定义
  45. class VersionType(Enum):
  46. SNAPSHOT = 'snapshot'
  47. PRE_RELEASE = 'pre'
  48. RELEASE_CANDIDATE = 'rc'
  49. RELEASE = 'release'
  50. OTHER = 'other'
  51. class MinecraftVersion:
  52. id: str
  53. type: VersionType
  54. url: str
  55. time: str
  56. release_time: str
  57. use_old_id_format: bool = False
  58. major_version: Optional[str] = None
  59. release_index: Optional[str] = None
  60. __server_jar_url: Optional[str] = None
  61. @property
  62. def server_jar_url(self) -> str:
  63. if self.__server_jar_url:
  64. return self.__server_jar_url
  65. server_jar_url: Optional[str]
  66. response = requests.get(self.url, timeout=10000)
  67. server_jar_url = json.loads(response.content).get('downloads').get('server').get('url')
  68. if not server_jar_url:
  69. raise ValueError(f"版本 {self.id} 的服务器端下载链接不存在")
  70. self.__server_jar_url = server_jar_url
  71. return server_jar_url
  72. __change_log_url: Optional[str] = None
  73. @property
  74. def change_log_url(self) -> str:
  75. """获取版本更新日志链接"""
  76. if self.__change_log_url:
  77. return self.__change_log_url
  78. if override_url := OFFICIAL_CHANGE_LOG_OVERRIDE_LINKS.get(self.id):
  79. self.__change_log_url = override_url
  80. return override_url
  81. if self.type == VersionType.OTHER:
  82. return ""
  83. if self.use_old_id_format:
  84. self.__change_log_url = OFFICIAL_CHANGELOG_URL_PREFIX + '-snapshot-' + self.id.lower()
  85. elif self.type == VersionType.RELEASE:
  86. self.__change_log_url = f"{OFFICIAL_CHANGELOG_URL_PREFIX}-java-edition-{self.id.replace('.', '-')}"
  87. else:
  88. assert self.major_version is not None
  89. self.__change_log_url = f"{OFFICIAL_CHANGELOG_URL_PREFIX}-" \
  90. f"{self.major_version.replace('.', '-')}-" \
  91. f"{self.type.name.lower().replace('_', '-')}-{self.release_index}"
  92. return self.__change_log_url
  93. def __init__(self, version_id: str, url: str, time: str, release_time: str):
  94. self.id = version_id
  95. self.url = url
  96. self.time = time
  97. self.release_time = release_time
  98. if m := re.match(DEVELOP_VERSION_PATTERN, version_id):
  99. self.type = VersionType(m.group('type').lower())
  100. self.major_version = m.group('major')
  101. self.release_index = m.group('index')
  102. elif re.match(OLD_SNAPSHOT_PATTERN, version_id):
  103. self.type = VersionType.SNAPSHOT
  104. self.use_old_id_format = True
  105. elif re.match(RELEASE_PATTERN, version_id):
  106. self.type = VersionType.RELEASE
  107. self.major_version = version_id
  108. else:
  109. self.type = VersionType.OTHER
  110. class MinecraftVersionManager:
  111. __latest_development_version_id: Optional[str]
  112. __latest_release_version_id: Optional[str]
  113. __manifest: MinecraftManifest
  114. __manifest_versions: list[MinecraftManifestVersionInfo]
  115. __versions: dict[str, MinecraftVersion] = {}
  116. __version_ids: list[str] = []
  117. def __init__(self):
  118. self.refresh_manifest()
  119. self.__latest_development_version_id = self.__manifest['latest']['snapshot']
  120. self.__latest_release_version_id = self.__manifest['latest']['release']
  121. self.__manifest_versions = self.__manifest.get('versions', [])
  122. logger.info(f"最新快照版:{self.__latest_development_version_id}, "
  123. f"最新正式版:{self.__latest_release_version_id}")
  124. self.__version_ids = [version['id'] for version in self.__manifest_versions]
  125. def get(self, version_id: str) -> Optional[MinecraftVersion]:
  126. if version_id in self.__versions:
  127. return self.__versions[version_id]
  128. for version_info in self.__manifest_versions:
  129. if version_info['id'] == version_id:
  130. version = MinecraftVersion(version_id=version_info['id'],
  131. url=version_info['url'],
  132. time=version_info['time'],
  133. release_time=version_info['releaseTime'])
  134. self.__versions[version_id] = version
  135. return version
  136. logger.warning(f"版本 {version_id} 不存在于版本列表中")
  137. return None
  138. def get_version_id_list(self) -> list[str]:
  139. return self.__version_ids
  140. def get_latest_version(self, version_type: Literal['any', 'development', 'release'] = 'any'
  141. ) -> Optional[MinecraftVersion]:
  142. if version_type == 'any':
  143. if self.__latest_development_version_id:
  144. return self.get(self.__latest_development_version_id)
  145. elif self.__latest_release_version_id:
  146. return self.get(self.__latest_release_version_id)
  147. else:
  148. return None
  149. elif version_type == 'development' and self.__latest_development_version_id:
  150. return self.get(self.__latest_development_version_id)
  151. elif version_type == 'release' and self.__latest_release_version_id:
  152. return self.get(self.__latest_release_version_id)
  153. else:
  154. return None
  155. @property
  156. def versions(self):
  157. for version_id in self.__version_ids:
  158. version = self.get(version_id)
  159. assert version is not None
  160. yield version
  161. def get_latest_development_versions(self) -> Optional[list[MinecraftVersion]]:
  162. latest_version = self.get_latest_version()
  163. if not latest_version or latest_version.type == VersionType.RELEASE:
  164. return None
  165. latest_development_versions = []
  166. current_major_version_branches = set()
  167. for version in self.versions:
  168. if version.major_version in current_major_version_branches:
  169. break
  170. if version.type == VersionType.RELEASE:
  171. break
  172. if version.type == VersionType.OTHER:
  173. continue
  174. current_major_version_branches.add(version.major_version)
  175. latest_development_versions.append(version)
  176. return latest_development_versions
  177. def refresh_manifest(self):
  178. self.__manifest = self.fetch_manifest()
  179. @classmethod
  180. def fetch_manifest(cls) -> MinecraftManifest:
  181. logger.info("正在加载 MC 版本列表")
  182. try:
  183. response = requests.get(LAUNCHER_MANIFEST_URL, timeout=10000)
  184. return json.loads(response.content)
  185. except Exception as e:
  186. logger.warning("加载 MC 版本列表失败")
  187. logger.exception(e)
  188. return MinecraftManifest(latest=MinecraftManifestLatestVersionInfo(snapshot=None, release=None),
  189. versions=[])
  190. # endregion
  191. MCVM = MinecraftVersionManager()
  192. # region 脚本接口
  193. @script('ServerJar')
  194. def get_server_jar_script(card, **_) -> str:
  195. """获取该卡片MC版本服务器端 jar 包下载链接"""
  196. version = MCVM.get(card['version-id'])
  197. assert version is not None, f"无法获取版本 {card['version-id']} 的信息,无法获取服务器端下载链接"
  198. return version.server_jar_url
  199. @script('MainPageVersions')
  200. def main_page_versions_script(page_area, **_) -> str:
  201. """获取特定区域的最新 MC 版本的卡片引用"""
  202. latest_development_versions = MCVM.get_latest_development_versions()
  203. latest_version_is_dev_version = bool(latest_development_versions)
  204. if page_area == 'new':
  205. if latest_version_is_dev_version:
  206. version_card_refereces = \
  207. [f"{v.id} | latest = true | s = false" for v in latest_development_versions]
  208. return ';'.join(version_card_refereces)
  209. else:
  210. latest_release = MCVM.get_latest_version(version_type='release')
  211. assert latest_release is not None, "无法获取最新正式版信息"
  212. return f"{latest_release.id} | latest = true | s = false"
  213. else:
  214. if latest_version_is_dev_version:
  215. latest_release = MCVM.get_latest_version(version_type='release')
  216. assert latest_release is not None, "无法获取最新正式版信息"
  217. return f"{latest_release.id} | latest = true"
  218. else:
  219. return ''
  220. @script('VersionLatestList')
  221. def version_latest_list_script(**_):
  222. """获取最新版本展示列表(用于新闻主页精简版)"""
  223. context = Context.get_current_context()
  224. library = context.project.base_library
  225. components = context.components
  226. code = ''
  227. version_type: Literal['development', 'release']
  228. for version_type in ['release', 'development']:
  229. if latest_version := MCVM.get_latest_version(version_type=version_type):
  230. version_card = library.get_card(latest_version.id, False)
  231. code += components['VersionLinks/Latest'].toxaml(card=version_card)
  232. return code
  233. @script('VersionArchiveList')
  234. def version_archive_list_script(cat_name, card, **_):
  235. context = Context.get_current_context()
  236. components = context.components
  237. cat_name = format_code(code=cat_name, data=card)
  238. cards = list(filter(lambda c: isinstance(c.get('cats'), list)
  239. and cat_name in c.get('cats'), context.project.get_all_card()))
  240. code = '<StackPanel Margin="8,0,8,0">'
  241. cards.sort(key=lambda c: MCVM.get_version_id_list().index(format_code(c['version-id'], data=c)))
  242. first_card_version_type = format_code(cards[0]['version-type-id'], cards[0])
  243. if len(cards) > 0 and first_card_version_type not in ['Release', 'April-Fools']:
  244. code += components['VersionLinks/Future'].toxaml(card={})
  245. for version_card in cards:
  246. if version_card.get('lack'):
  247. code += components['VersionLinks/Lack'].toxaml(card=version_card)
  248. else:
  249. code += components['VersionLinks/Common'].toxaml(card=version_card)
  250. code += '</StackPanel>'
  251. return code
  252. @script('GetLink')
  253. def get_link(link_type, card, link_key=None, **kwargs):
  254. name: str = card['version-id']
  255. name = format_code(name, card)
  256. if link_type == 'Official':
  257. return MCVM.get(name).change_log_url
  258. if card.get('wip') == 'true' and link_type != 'Official':
  259. return ''
  260. data = Context.get_current_context().data[f'{link_type}Link']
  261. if link_key: # 如果指定查找键
  262. return data[link_key]
  263. return data.get(name, "")
  264. # endregion