downloads an entire comic from yiffer.xyz.
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)