Created
August 15, 2025 09:54
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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