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
All checks were successful
Gitea/kapitanbooru-uploader/pipeline/head This commit looks good
This commit is contained in:
@@ -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()
|
||||
|
Reference in New Issue
Block a user