upload_header_image.py 5.4 KB

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