Profile

yifferdownloader

← Back to repositories | View on GitHub

downloads an entire comic from yiffer.xyz.

Python 0 0 Updated: 2/22/2026

main.py

import requests
from bs4 import BeautifulSoup
import os
import re
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

# GUI imports
try:
    import tkinter as tk
    from tkinter import ttk, messagebox
    GUI_AVAILABLE = True
except ImportError:
    GUI_AVAILABLE = False

def sanitize_folder_name(name):
    name = name.replace("%20", "_")
    name = re.sub(r'[<>:"/\\|?*]', '_', name)
    return name.strip("_")

def format_time(seconds):
    if seconds < 0:
        seconds = 0
    m, s = divmod(int(seconds), 60)
    h, m = divmod(m, 60)
    if h > 0:
        return f"{h}h {m}m {s}s"
    elif m > 0:
        return f"{m}m {s}s"
    else:
        return f"{s}s"

def download_image(img_info, headers, folder_name):
    idx, img_url = img_info
    try:
        if img_url.startswith("//"):
            img_url = "https:" + img_url
        elif img_url.startswith("/"):
            img_url = "https://yiffer.xyz" + img_url

        file_ext = os.path.splitext(img_url)[1]
        file_name = f"{idx:03d}{file_ext}"
        file_path = os.path.join(folder_name, file_name)

        img_data = requests.get(img_url, headers=headers).content
        with open(file_path, "wb") as f:
            f.write(img_data)
        return idx, True, None
    except Exception as e:
        return idx, False, str(e)

def download_yiffer_comic(url, folder_name, progress_callback=None, status_callback=None, max_workers=5):
    headers = {
        "User-Agent": "Yiffer.xyz downloader at https://github.com/GattoDev-debug/yifferdownloader"
    }

    os.makedirs(folder_name, exist_ok=True)

    try:
        response = requests.get(url, headers=headers)
        if response.status_code != 200:
            msg = f"Failed to fetch page. Status code: {response.status_code}"
            if status_callback:
                status_callback(msg)
            else:
                print(msg)
            return

        soup = BeautifulSoup(response.text, "html.parser")
        img_tags = soup.find_all("img", class_="comicPage")

        if not img_tags:
            msg = "No images found with class 'comicPage'."
            if status_callback:
                status_callback(msg)
            else:
                print(msg)
            return

        total = len(img_tags)
        img_urls = [(i, img["src"]) for i, img in enumerate(img_tags, 1)]

        start_time = time.time()
        completed = 0
        lock = threading.Lock()

        if status_callback:
            status_callback(f"Starting download of {total} images with {max_workers} threads...")

        def progress_update(idx, success, error_msg):
            nonlocal completed
            with lock:
                completed += 1
                elapsed = time.time() - start_time
                avg_time = elapsed / completed
                remaining = avg_time * (total - completed)

                status_text = (
                    f"Downloading {completed} of {total} images | "
                    f"Elapsed: {format_time(elapsed)} | "
                    f"ETA: {format_time(remaining)}"
                )
                if status_callback:
                    status_callback(status_text)
                if progress_callback:
                    progress_callback(completed, total)

                if not success and error_msg:
                    print(f"Failed to download image #{idx}: {error_msg}")

        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = []
            for info in img_urls:
                futures.append(executor.submit(download_image, info, headers, folder_name))

            for future in as_completed(futures):
                idx, success, error_msg = future.result()
                progress_update(idx, success, error_msg)

        done_msg = f"Download complete. Saved to folder: {folder_name}"
        if status_callback:
            status_callback(done_msg)
        else:
            print(done_msg)

    except Exception as e:
        err_msg = "Error: " + str(e)
        if status_callback:
            status_callback(err_msg)
            if GUI_AVAILABLE:
                messagebox.showerror("Error", err_msg)
        else:
            print(err_msg)

# --- GUI Functions ---

def start_download_gui():
    url = url_entry.get().strip()
    folder = folder_entry.get().strip()

    if not url:
        messagebox.showwarning("Warning", "Please enter a URL.")
        return

    if not folder:
        raw_folder = url.strip("/").split("/")[-1]
        folder = sanitize_folder_name(raw_folder)
    else:
        folder = sanitize_folder_name(folder)

    progress_bar["value"] = 0
    start_button.config(state=tk.DISABLED)
    status_label.config(text="Starting download...")

    def run():
        download_yiffer_comic(url, folder, update_progress, update_status)
        start_button.config(state=tk.NORMAL)

    threading.Thread(target=run, daemon=True).start()

def update_progress(current, total):
    progress = (current / total) * 100
    progress_bar["value"] = progress
    root.update_idletasks()

def update_status(text):
    status_label.config(text=text)

def run_gui():
    global root, url_entry, folder_entry, start_button, status_label, progress_bar
    root = tk.Tk()
    root.title("Yiffer.xyz Downloader")

    tk.Label(root, text="Comic URL:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
    url_entry = tk.Entry(root, width=50)
    url_entry.grid(row=0, column=1, padx=5, pady=5)

    tk.Label(root, text="Folder Name (optional):").grid(row=1, column=0, padx=5, pady=5, sticky="e")
    folder_entry = tk.Entry(root, width=50)
    folder_entry.grid(row=1, column=1, padx=5, pady=5)

    start_button = tk.Button(root, text="Start Download", command=start_download_gui)
    start_button.grid(row=2, column=0, columnspan=2, pady=10)

    status_label = tk.Label(root, text="Idle")
    status_label.grid(row=3, column=0, columnspan=2)

    progress_bar = ttk.Progressbar(root, length=400, mode='determinate')
    progress_bar.grid(row=4, column=0, columnspan=2, pady=10)

    root.mainloop()

# --- CLI Functions ---

def run_cli(url):
    if not url:
        url = input("Enter Yiffer comic URL: ").strip()
        if not url:
            print("No URL provided. Exiting.")
            return

    raw_folder = url.strip("/").split("/")[-1]
    folder = sanitize_folder_name(raw_folder)
    download_yiffer_comic(url, folder)

# --- Main ---

if __name__ == "__main__":
    if len(sys.argv) > 1:
        # CLI mode with URL as argument
        run_cli(sys.argv[1])
    else:
        # No CLI argument — run GUI if available, else fallback to CLI prompt
        if GUI_AVAILABLE:
            run_gui()
        else:
            print("GUI not available. Running in CLI mode.")
            run_cli(None)