upload_header_image.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import os
  2. import sys
  3. import boto3
  4. import requests
  5. from bs4 import BeautifulSoup
  6. from botocore.config import Config
  7. from minecraft_version import MCVM, MinecraftVersion
  8. # ============================================================
  9. # 配置
  10. # ============================================================
  11. USER_AGENT = (
  12. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
  13. "AppleWebKit/537.36 (KHTML, like Gecko) "
  14. "Chrome/139.0 Safari/537.36"
  15. )
  16. # ============================================================
  17. # R2
  18. # ============================================================
  19. def create_r2_client():
  20. account_id = os.environ["R2_ACCOUNT_ID"]
  21. access_key = os.environ["R2_ACCESS_KEY_ID"]
  22. secret_key = os.environ["R2_SECRET_ACCESS_KEY"]
  23. return boto3.client(
  24. "s3",
  25. endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
  26. aws_access_key_id=access_key,
  27. aws_secret_access_key=secret_key,
  28. region_name="auto",
  29. config=Config(
  30. signature_version="s3v4"
  31. ),
  32. )
  33. # ============================================================
  34. # 获取更新日志 URL
  35. # ============================================================
  36. def get_change_log_url(version_id: str) -> str:
  37. version = MCVM.get(version_id)
  38. if version is None:
  39. raise ValueError(
  40. f"MC 版本 {version_id} 不存在"
  41. )
  42. url = version.change_log_url
  43. if not url:
  44. raise ValueError(
  45. f"MC 版本 {version_id} 没有更新日志链接"
  46. )
  47. #print(f"版本: {version.id}")
  48. #print(f"更新日志: {url}")
  49. return url
  50. # ============================================================
  51. # 下载 HTML
  52. # ============================================================
  53. def download_html(url: str) -> str:
  54. #print("[1/4] 下载 HTML...")
  55. headers = {
  56. "User-Agent": USER_AGENT,
  57. "Referer": "https://www.minecraft.net/",
  58. "Accept": (
  59. "text/html,application/xhtml+xml,"
  60. "application/xml;q=0.9,*/*;q=0.8"
  61. ),
  62. }
  63. response = requests.get(
  64. url,
  65. headers=headers,
  66. timeout=30,
  67. )
  68. response.raise_for_status()
  69. response.encoding = response.apparent_encoding
  70. return response.text
  71. def find_hero_image(html: str, page_url: str) -> str:
  72. #print("[2/4] 获取图片路径...")
  73. soup = BeautifulSoup(
  74. html,
  75. "html.parser",
  76. )
  77. poster = soup.select_one(
  78. "div.MC_articleHeroA_poster"
  79. )
  80. if poster is None:
  81. raise RuntimeError(
  82. "找不到 div.MC_articleHeroA_poster"
  83. )
  84. img = poster.select_one(
  85. "picture img"
  86. )
  87. if img is None:
  88. raise RuntimeError(
  89. "找不到 picture img"
  90. )
  91. src = img.get("src")
  92. if not src:
  93. raise RuntimeError(
  94. "img 没有 src 属性"
  95. )
  96. from urllib.parse import urljoin
  97. image_url = urljoin(
  98. page_url,
  99. src,
  100. )
  101. #print(f"图片: {image_url}")
  102. return image_url
  103. def download_image(
  104. image_url: str,
  105. page_url: str,
  106. ) -> tuple[bytes, str]:
  107. #print("[3/4] 下载图片...")
  108. headers = {
  109. "User-Agent": USER_AGENT,
  110. "Referer": page_url,
  111. "Accept": (
  112. "image/avif,image/webp,image/apng,"
  113. "image/svg+xml,image/*,*/*;q=0.8"
  114. ),
  115. }
  116. response = requests.get(
  117. image_url,
  118. headers=headers,
  119. timeout=60,
  120. )
  121. response.raise_for_status()
  122. content_type = response.headers.get(
  123. "Content-Type",
  124. "application/octet-stream",
  125. )
  126. return response.content, content_type
  127. def upload_to_r2(
  128. data: bytes,
  129. bucket: str,
  130. object_key: str,
  131. content_type: str,
  132. ):
  133. #print("[4/4] 上传 Cloudflare R2...")
  134. s3 = create_r2_client()
  135. s3.put_object(
  136. Bucket=bucket,
  137. Key=object_key,
  138. Body=data,
  139. ContentType=content_type,
  140. )
  141. def upload_header_image(version):
  142. if isinstance(version, str):
  143. version = MCVM.get(version)
  144. if not isinstance(version, MinecraftVersion):
  145. raise TypeError()
  146. change_log_url = version.change_log_url
  147. html = download_html(
  148. change_log_url
  149. )
  150. image_url = find_hero_image(
  151. html,
  152. change_log_url,
  153. )
  154. image_data, content_type = download_image(
  155. image_url,
  156. change_log_url,
  157. )
  158. bucket = "images"
  159. object_key = f"news/version/{version.major_version}/{version.id}.webp"
  160. upload_to_r2(
  161. image_data,
  162. bucket,
  163. object_key,
  164. content_type,
  165. )
  166. return f"https://image.bugjump.net/{object_key}"
  167. def main():
  168. if len(sys.argv) != 2:
  169. print(
  170. f"用法: {sys.argv[0]} <Minecraft版本号>"
  171. )
  172. print()
  173. print("例如:")
  174. print(f" {sys.argv[0]} 26.3")
  175. print(f" {sys.argv[0]} 26.3-rc-1")
  176. sys.exit(1)
  177. version_id = sys.argv[1]
  178. version = MCVM.get(version_id)
  179. if not version:
  180. raise ValueError('错误:未找到版本')
  181. url = upload_header_image(version)
  182. print(f"已上传至{url}")
  183. if __name__ == "__main__":
  184. main()