Skip to content

Instantly share code, notes, and snippets.

@noaione
Created August 15, 2025 09:54
Show Gist options
  • Save noaione/3e83c19b5ac252d56db025a404010bfd to your computer and use it in GitHub Desktop.
Save noaione/3e83c19b5ac252d56db025a404010bfd to your computer and use it in GitHub Desktop.
quick tools to download stuff since i hate using hakuneko/haruneko because it's bloated with too many stuff (when I just need downloader)
import json
import tomllib
from base64 import b64decode
from datetime import datetime
from io import BytesIO
from pathlib import Path
import requests
import tomli_w
from PIL import Image
HOST = "https://comic-growl.com"
TOKENS_URL = "https://id.comici.jp/auth/tokens"
ROOT_VIEWER = "" # get this by checking request
ROOT_DIR = Path(__file__).parent.resolve()
OUTPUT_DIR = ROOT_DIR / "output"
OUTPUT_DIR.mkdir(exist_ok=True)
# Read config.toml
config_toml = ROOT_DIR / "config.toml"
if not config_toml.exists():
raise FileNotFoundError(f"Configuration file {config_toml} not found.")
with config_toml.open("rb") as fp:
config = tomllib.load(fp)
session = requests.Session()
session.headers.update({
"Cookie": f"session={config['session']}",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:141.0) Gecko/20100101 Firefox/141.0",
})
def reauth_if_possible(current_id_token: str, refresh_token: str):
decoded_token = json.loads(b64decode(current_id_token.split(".")[1] + "==").decode("utf-8"))
# check expiry
now = datetime.now().timestamp()
if now < decoded_token["exp"]:
return current_id_token, refresh_token
print("ID token expired, refreshing...")
response = session.post(
TOKENS_URL,
json={
"idToken": current_id_token,
"refreshToken": refresh_token,
}
)
response.raise_for_status()
tokens = response.json()
new_id_token = tokens["idToken"]
new_refresh_token = tokens["refreshToken"]
return new_id_token, new_refresh_token
def get_auth_token():
new_id_token, new_refresh_token = reauth_if_possible(config["id-token"], config["refresh-token"])
# resave config with new tokens
config["id-token"] = new_id_token
config["refresh-token"] = new_refresh_token
with config_toml.open("wb") as fp:
tomli_w.dump(config, fp)
return new_id_token
def get_episodes_list(viewer_id: str):
auth_token = get_auth_token()
response = session.get(
f"{HOST}/book/episodeInfo",
params={"comici-viewer-id": viewer_id, "isPreview": "false"},
headers={"Authorization": auth_token}
)
response.raise_for_status()
return response.json()
def get_images_list(viewer_id: str, maximum_page: int):
auth_token = get_auth_token()
response = session.get(
f"{HOST}/book/contentsInfo",
params={"comici-viewer-id": viewer_id, "user-id": str(config["id"]), "page-from": "0", "page-to": str(maximum_page)},
headers={"Authorization": auth_token}
)
response.raise_for_status()
return response.json()
def maybe_descramble(image_data: bytes, scramble_key: str | None = None) -> bytes:
if not scramble_key:
return image_data
# example scramble_key
# scramble_key = "[8, 1, 9, 5, 4, 6, 7, 3, 2, 10, 0, 12, 11, 14, 13, 15]"
### scrambleMatrix = new Array(16).fill(null).map((_, index) => [ index / 4 >> 0, index % 4 >> 0 ]);
scramble_matrix = [(index // 4, index % 4) for index in range(16)]
scramble_key_parsed: list[int] = json.loads(scramble_key)
decoded_matrix = []
for key in scramble_key_parsed:
decoded_matrix.append(scramble_matrix[key])
img = Image.open(BytesIO(image_data))
tile_w = img.width // 4
tile_h = img.height // 4
output_img = Image.new(img.mode, img.size)
keybox = 0
for x in range(4):
for y in range(4):
src_x = tile_w * decoded_matrix[keybox][0]
src_y = tile_h * decoded_matrix[keybox][1]
tile = img.crop((src_x, src_y, src_x + tile_w, src_y + tile_h))
dst_x = tile_w * x
dst_y = tile_h * y
output_img.paste(tile, (dst_x, dst_y))
keybox += 1
output_buffer = BytesIO()
output_img.save(output_buffer, format="png")
output_buffer.seek(0)
return output_buffer.read()
def secure_filename(folder_name: str) -> str:
disallowed_chars = r'<>:"/\|?*'
for char in disallowed_chars:
folder_name = folder_name.replace(char, "_")
return folder_name.strip()
def main():
# episodes = get_episodes_list(ROOT_VIEWER)
episodes = get_episodes_list(ROOT_VIEWER)
# sort by episode_number
episodes_sorted = sorted(episodes["result"], key=lambda x: int(x["episode_number"]))
for episode in episodes_sorted:
title = episode["name"]
uuid = episode["id"]
print(f"Processing episode: {title} ({uuid})")
page_count = int(episode["page_count"])
sort_number = int(episode["episode_number"])
# save dir
folder_target = secure_filename(f"c{sort_number:03d} - {title}")
episode_dir = OUTPUT_DIR / folder_target
episode_dir.mkdir(parents=True, exist_ok=True)
images_raw = get_images_list(uuid, page_count)
sorted_images = sorted(images_raw["result"], key=lambda x: x["sort"])
for p_idx, image in enumerate(sorted_images):
img_url = image["imageUrl"]
scramble_data = image.get("scramble")
print(f"Downloading image: {image['imageUrl']}")
response = session.get(img_url, headers={
"Origin": f"{HOST}",
"Referer": f"{HOST}/",
})
response.raise_for_status()
# maybe descramble
image_data = maybe_descramble(response.content, scramble_data)
# save the image
output_path = episode_dir / f"p{p_idx:03d}.png"
with output_path.open("wb") as img_file:
img_file.write(image_data)
if __name__ == "__main__":
main()
id = 00000000 # get this by checking request
session = "" # get this from your SESSION cookie
# get both of this from localStorage
id-token = ""
refresh-token = ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment