Bump version to 0.8.0 and add support for all image formats
All checks were successful
Gitea/kapitanbooru-uploader/pipeline/head This commit looks good

This commit is contained in:
2025-03-27 21:11:36 +01:00
parent dcd8e0824f
commit b1b8ed237e
6 changed files with 450 additions and 360 deletions

View File

@@ -1,148 +1,38 @@
import glob
import hashlib
import inspect
import os
import queue
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from typing import Dict, Tuple, Optional
import concurrent.futures
from packaging.version import parse as parse_version
import itertools
import networkx as nx
import requests
from PIL import Image, ImageTk, PngImagePlugin
from PIL import Image, ImageTk
import wdtagger as wdt
import tomli
from .ProcessingDialog import ProcessingDialog
from .I18N import _
from .ProgressFile import ProgressFile
from .TagsRepo import TagsRepo
from .autocomplete import TagManager
from .common import get_auth_token, login, open_tag_wiki_url, open_webbrowser
from .settings import Settings
from .tag_processing import TAG_FIXES, parse_parameters, process_tag
from .tag_processing import TAG_FIXES, extract_parameters, parse_parameters, process_tag
from .tagger_cache import TaggerCache
class ProcessingDialog:
def __init__(self, root, target_function, *args):
self.root = root
self.top = tk.Toplevel(root)
self.top.title(_("Processing..."))
self.top.geometry("300x150")
self.top.protocol("WM_DELETE_WINDOW", self.on_close)
self.label = tk.Label(self.top, text=_("Processing, please wait..."))
self.label.pack(pady=10)
# Start with indeterminate progress bar
self.progress = ttk.Progressbar(self.top, mode="indeterminate")
self.progress.pack(pady=10, fill="x")
self.progress.start(10)
sig = inspect.signature(target_function)
if "secondary_progress_queue" in sig.parameters:
self.sub_progress = ttk.Progressbar(self.top, mode="indeterminate")
self.sub_progress.pack(pady=10, fill="x")
self.sub_progress.start(10)
# Setup communication queue and periodic checker
self.queue = queue.Queue()
self.sub_queue = queue.Queue()
self.running = True
self.cancel_event = threading.Event() # Cancellation flag
self.thread = threading.Thread(
target=self.run_task, args=(target_function, *args)
)
self.thread.start()
self.top.after(100, self.process_queue)
def process_queue(self):
"""Process messages from the background thread"""
while self.running:
try:
msg = self.queue.get_nowait()
if msg[0] == "mode":
self.progress.config(mode=msg[1])
if msg[1] == "determinate":
self.progress["value"] = 0
self.progress.stop()
elif msg[1] == "indeterminate":
self.progress["value"] = 0
self.progress.start()
elif msg[0] == "max":
self.progress["maximum"] = msg[1]
elif msg[0] == "progress":
self.progress["value"] = msg[1]
elif msg[0] == "label":
self.label.config(text=msg[1])
self.top.update_idletasks()
except queue.Empty:
break
try:
msg = self.sub_queue.get_nowait()
if msg[0] == "mode":
self.sub_progress.config(mode=msg[1])
if msg[1] == "determinate":
self.sub_progress["value"] = 0
self.sub_progress.stop()
elif msg[1] == "indeterminate":
self.sub_progress["value"] = 0
self.sub_progress.start()
elif msg[0] == "max":
self.sub_progress["maximum"] = msg[1]
elif msg[0] == "progress":
self.sub_progress["value"] = msg[1]
self.top.update_idletasks()
except queue.Empty:
break
if self.running:
self.top.after(100, self.process_queue)
def run_task(self, target_function, *args):
"""Execute target function with progress queue if supported"""
try:
sig = inspect.signature(target_function)
kwargs = {}
if "progress_queue" in sig.parameters:
kwargs["progress_queue"] = self.queue
if "secondary_progress_queue" in sig.parameters:
kwargs["secondary_progress_queue"] = self.sub_queue
if "cancel_event" in sig.parameters:
kwargs["cancel_event"] = self.cancel_event
target_function(*args, **kwargs)
finally:
self.close_dialog()
def close_dialog(self):
"""Safely close the dialog"""
if self.running:
self.running = False
self.progress.stop()
self.top.after(0, self.top.destroy)
self.root.after(100, self.thread.join)
def on_close(self):
"""Handle manual window closure"""
self.running = False
self.cancel_event.set() # Notify target function that cancellation is requested
self.top.destroy()
class ImageBrowser(tk.Tk):
def __init__(self):
super().__init__()
self.title("Kapitanbooru Uploader")
self.geometry("900x600")
self.version = "0.7.0"
self.version = "0.8.0"
self.acknowledged_version = parse_version(self.version)
self.settings = Settings()
@@ -359,9 +249,7 @@ class ImageBrowser(tk.Tk):
# Pobierz tagi z pliku
try:
img = Image.open(file_path)
parameters = ""
if isinstance(img, PngImagePlugin.PngImageFile):
parameters = img.info.get("parameters", "")
parameters = extract_parameters(img, file_path)
png_tags = set(
[
x
@@ -785,27 +673,37 @@ class ImageBrowser(tk.Tk):
def select_folder(self):
"""
Otwiera okno dialogowe wyboru folderu z obrazkami
i wczytuje pliki PNG z wybranego folderu.
i wczytuje pliki PNG, JPEG, WebP, AVIF i GIF z wybranego folderu.
"""
folder = filedialog.askdirectory(title=_("Wybierz folder z obrazkami PNG"))
folder = filedialog.askdirectory(
title=_("Wybierz folder z obrazkami PNG, JPEG, WebP, AVIF i GIF")
)
if folder:
self.folder_path = folder
self.load_images()
def load_images(self):
"""
Ładuje pliki PNG z wybranego folderu.
Ładuje pliki PNG, JPEG, WebP, AVIF i GIF z wybranego folderu.
"""
pattern = os.path.join(self.folder_path, "*.png")
self.image_files = sorted(glob.glob(pattern))
extensions = ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.avif", "*.gif")
self.image_files = sorted(
file
for ext in extensions
for file in glob.glob(os.path.join(self.folder_path, ext), recursive=True)
)
self.total_files = len(self.image_files)
self.image_files_md5 = {
file: self.compute_md5(file) for file in self.image_files
file: md5
for file, md5 in zip(
self.image_files, self.compute_md5_parallel(self.image_files)
)
}
self.tagger_processed.clear()
for md5 in self.image_files_md5.values():
if self.tagger_cache[md5]:
self.tagger_processed.add(md5)
# Clear the entire listbox to prepare for reloading new items
self.listbox.delete(0, tk.END)
self.uploaded.clear()
self.upload_verified = 0
@@ -820,7 +718,8 @@ class ImageBrowser(tk.Tk):
self.post_load_processing()
else:
messagebox.showinfo(
_("Informacja"), _("Brak plików PNG w wybranym folderze.")
_("Informacja"),
_("Brak plików PNG, JPEG, WebP, AVIF lub GIF w wybranym folderze."),
)
def post_load_processing(self):
@@ -972,17 +871,22 @@ class ImageBrowser(tk.Tk):
open_webbrowser(url, self.settings)
def compute_md5(self, file_path, chunk_size=8192):
"""Oblicza MD5 dla danego pliku."""
"""Compute MD5 for a single file."""
hash_md5 = hashlib.md5()
try:
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(chunk_size), b""):
hash_md5.update(chunk)
except Exception as e:
print(_("Błąd przy obliczaniu MD5:"), e)
print(_("Error computing MD5:"), e)
return ""
return hash_md5.hexdigest()
def compute_md5_parallel(self, file_paths):
"""Compute MD5 for multiple files in parallel."""
with concurrent.futures.ThreadPoolExecutor() as executor:
return list(executor.map(self.compute_md5, file_paths))
def on_listbox_select(self, event):
"""
Wywoływane po wybraniu pliku z listy.
@@ -1019,9 +923,7 @@ class ImageBrowser(tk.Tk):
file_path = self.image_files[index]
try:
img = Image.open(file_path)
parameters = ""
if isinstance(img, PngImagePlugin.PngImageFile):
parameters = img.info.get("parameters", "")
parameters = extract_parameters(img, file_path)
self.current_image_original = img.copy()
self.current_parameters = parameters
self.update_display_image()