From 67df3ea793fdbe8af8a67b3a5ad9db08ebe5296e Mon Sep 17 00:00:00 2001 From: Kapitan Date: Thu, 26 Jun 2025 17:22:34 +0200 Subject: [PATCH] Update Kapitanbooru Uploader to version 0.9.0 with significant changes - Updated version number in pyproject.toml and messages.po files. - Added new translations and updated existing ones in messages.po for better localization. - Implemented core functionality in Core.py, including image processing, tagging, and upload logic. - Enhanced the autotagging feature to support multiple image formats (PNG, JPEG, WebP, AVIF, GIF). - Improved error handling and logging for file operations and network requests. - Added functionality to check for uploaded files and manage tag updates for existing posts. - Introduced threading for background processing to improve application responsiveness. --- kapitanbooru_uploader/Core.py | 744 +++++++++++++++ kapitanbooru_uploader/ImageBrowser.py | 862 ++++-------------- kapitanbooru_uploader/__main__.py | 36 +- .../locales/en/LC_MESSAGES/messages.po | 580 ++++++------ .../locales/pl/LC_MESSAGES/messages.po | 362 ++++---- pyproject.toml | 2 +- 6 files changed, 1463 insertions(+), 1123 deletions(-) create mode 100644 kapitanbooru_uploader/Core.py diff --git a/kapitanbooru_uploader/Core.py b/kapitanbooru_uploader/Core.py new file mode 100644 index 0000000..75a7237 --- /dev/null +++ b/kapitanbooru_uploader/Core.py @@ -0,0 +1,744 @@ +import glob +import hashlib +import os +import threading +from typing import Any, Callable, Tuple +import concurrent.futures +from packaging.version import parse as parse_version +import queue +import time + +import networkx as nx +import requests +from PIL import Image +import wdtagger as wdt + +from .I18N import _ +from .TagsRepo import TagsRepo +from .settings import Settings +from .common import login, get_auth_token +from .ProgressFile import ProgressFile +from .tag_processing import ( + TAG_FIXES, + extract_parameters, + parse_parameters, + process_tag, + extract_artist_from_filename, +) +from .tagger_cache import TaggerCache + + +class Core: + """ + Core functionality for Kapitanbooru Uploader. + Handles image processing, tagging, and upload logic. + """ + + def __init__(self, settings: Settings, gui_mode: bool = True): + self.version = "0.9.0" + self.acknowledged_version = parse_version(self.version) + self.settings = settings + self.tags_repo = TagsRepo(settings) + # Dodatkowe ustawienia dla Taggera + self.tagger_name = "wdtagger" + self.tagger_version = ( + "1.0" # możesz ustawić wersję dynamicznie, jeśli to możliwe + ) + self.tagger_cache = TaggerCache( + self.settings, self.tagger_name, self.tagger_version + ) + self.gui_mode = gui_mode + self.main_thread_queue = queue.Queue() + self.after_events = [] + + self.implication_graph = self.load_implication_graph() + self.missing_tags = set() # Track tags not in the graph + + self.check_uploaded_files_stop_event = threading.Event() + self.check_uploaded_files_thread: threading.Thread | None = None + self.process_tagger_queue_stop_event = threading.Event() + self.process_tagger_queue_thread: threading.Thread | None = None + + self.check_uploaded_files_callback = None + self.update_status_bar_callback = None + self.process_tagger_for_image_callback = None + self.upload_file_success_callback: Callable[[str], Any] | None = None + self.upload_file_completed_callback: Callable | None = None + + self.folder_path = "" + self.image_files = [] + self.image_files_md5 = [] + self.current_index = None + self.image_cache = None + self.tagger_thread_idx = 0 + self.tagger = wdt.Tagger() + + # Liczniki statusu + self.total_files = 0 + self.tagger_processed = set() + self.upload_verified = 0 + self.uploaded_count = 0 + + # Oryginalny obraz (do skalowania) + self.current_image_original = None + self.current_parameters = "" + + # Mapa ratingów: wyświetlana nazwa -> wartość wysyłana + self.rating_map = { + "General": "g", + "Sensitive": "s", + "Questionable": "q", + "Explicit": "e", + "Unrated": "", + } + + # Słowniki przechowujące stany tagów (dla PNG i Taggera) + self.png_tags_states = {} + self.tagger_tags_states = {} + + # Nowy słownik przechowujący informację, czy dany plik (ścieżka) został już uploadowany + self.uploaded = {} # key: file path, value: True/False + + def schedule_in_main_thread(self, func, delay_ms=0): + """Schedule a function to run in the main thread""" + if self.gui_mode: + # In GUI mode, use the after mechanism + self.after_events.append((time.time() + delay_ms / 1000, func)) + else: + # In non-GUI mode, add to queue for immediate execution + self.main_thread_queue.put(func) + + def process_main_thread_queue(self): + """Process pending main thread tasks""" + if self.gui_mode: + # Process scheduled events + now = time.time() + new_events = [] + for schedule_time, func in self.after_events: + if now >= schedule_time: + try: + func() + except Exception as e: + print(f"Error in scheduled task: {e}") + else: + new_events.append((schedule_time, func)) + self.after_events = new_events + else: + # Process all queued tasks in non-GUI mode + while not self.main_thread_queue.empty(): + try: + func = self.main_thread_queue.get_nowait() + func() + except queue.Empty: + break + except Exception as e: + print(f"Error in main thread task: {e}") + + def load_implication_graph(self) -> nx.DiGraph: + G = nx.DiGraph() + conn = self.tags_repo.get_conn() + cursor = conn.cursor() + + # Step 1: Add all tags from the 'tags' table + cursor.execute( + """ + SELECT + CASE category + WHEN 1 THEN 'artist:' || name + WHEN 3 THEN 'copyright:' || name + WHEN 4 THEN 'character:' || name + WHEN 5 THEN 'meta:' || name + ELSE name + END AS prefixed_name + FROM tags + """ + ) + db_tags = {row[0] for row in cursor.fetchall()} + G.add_nodes_from(db_tags) + + # Step 2: Add nodes from implications (antecedents/consequents not in 'tags' table) + cursor.execute("SELECT antecedent, consequent FROM tag_closure") + edge_tags = set() + for ant, cons in cursor.fetchall(): + edge_tags.add(ant) + edge_tags.add(cons) + G.add_nodes_from(edge_tags - db_tags) # Add tags only in implications + + # Step 3: Add edges + cursor.execute("SELECT antecedent, consequent FROM tag_closure") + G.add_edges_from(cursor.fetchall()) + + conn.close() + return G + + def compute_final_tags_and_rating_for_file( + self, file_path, update_status_callback, manual_tags=set() + ) -> Tuple[str, str]: + """ + Oblicza finalną listę tagów dla danego pliku oraz rating. + Łączy tagi z: + - pliku (PNG): parsowane przez parse_parameters, + - Taggera (wynik z cache lub wyliczony na bieżąco), + - ustawień (default tags), + - manualnych tagów (z pola manual_tags_entry), + oraz dodaje tag "meta:auto_upload". + Zwraca finalny ciąg tagów oraz rating. + """ + # Pobierz tagi z pliku + try: + img = Image.open(file_path) + parameters = extract_parameters(img, file_path) + artist_tag = extract_artist_from_filename(file_path) + png_tags = set( + [ + x + for x in parse_parameters(parameters, self.tags_repo).split() + if process_tag(x, self.tags_repo)[1] + is not None # Ignoruj nieistniejące tagi + ] + ) + if artist_tag: + png_tags.add("artist:" + artist_tag.replace(" ", "_").replace("\\", "")) + img.close() + except Exception as e: + print(_("Błąd przy otwieraniu pliku"), file_path, ":", e) + png_tags = set() + + # Pobierz tagi z Taggera – sprawdzając cache + result = self.get_tagger_results(file_path, update_status_callback) + tagger_tags = set() + rating = "Unrated" + tagger_tags.update( + ( + TAG_FIXES[tag] if tag in TAG_FIXES else tag + for tag in result.general_tag_data.keys() + ) + ) # Zamień nieprawidłowe tagi na poprawne + for t in result.character_tags: + full_tag = "character:" + t.replace(" ", "_").replace("\\", "") + # Zamień nieprawidłowe tagi na poprawne + if full_tag in TAG_FIXES: + full_tag = TAG_FIXES[full_tag] + tagger_tags.add(full_tag) + rating = self.map_tagger_rating(result) + + # Pobierz tagi z ustawień i manualne + default_tags = set(self.settings.default_tags.split()) + + # Finalna lista: suma wszystkich tagów + final_tags = default_tags.union(png_tags).union(tagger_tags).union(manual_tags) + final_tags.add("meta:auto_upload") + return " ".join(sorted(final_tags)), rating + + def get_tagger_results(self, file_path, callback) -> wdt.Result: + md5 = self.image_files_md5[file_path] + cached = self.tagger_cache[md5] + if cached: + self.tagger_processed.add(md5) + return cached["result"] + try: + with Image.open(file_path) as img: + result = self.tagger.tag(img) + self.tagger_cache[md5] = result + self.tagger_processed.add(md5) + callback() + print(_("Tagger przetworzył:"), f"{file_path}") + return result + except Exception as e: + print(_("Błąd Taggera dla"), file_path, ":", e) + + def map_tagger_rating(self, result: wdt.Result) -> str: + """ + Mapuje rating z Taggera na wartość używaną w Kapitanbooru. + """ + if result.rating == "general": + new_rating = "General" + elif result.rating == "sensitive": + new_rating = "Sensitive" + elif result.rating == "questionable": + new_rating = "Questionable" + elif result.rating == "explicit": + new_rating = "Explicit" + else: + new_rating = "Unrated" + return new_rating + + def load_images(self): + """ + Ładuje pliki PNG, JPEG, WebP, AVIF i GIF z wybranego folderu. + """ + 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: 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) + self.uploaded.clear() + self.upload_verified = 0 + self.uploaded_count = 0 + for file in self.image_files: + self.uploaded[file] = False + if self.image_files: + self.post_load_processing() + + def compute_md5(self, file_path, chunk_size=8192): + """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(_("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 post_load_processing(self): + """ + Po załadowaniu plików, sprawdza czy są jakieś pliki do uploadu oraz przetwarza Taggerem pliki. + """ + self.join_check_uploaded_files_thread() + self.check_uploaded_files_thread = threading.Thread( + target=self.check_uploaded_files + ) + self.check_uploaded_files_thread.start() + self.join_process_tagger_queue_thread() + self.process_tagger_queue_thread = threading.Thread( + target=self.process_tagger_queue + ) + self.process_tagger_queue_thread.start() + + def wait_for_completion(self): + """Wait for background threads to finish (non-GUI mode)""" + # Join the checking thread if running + if ( + self.check_uploaded_files_thread + and self.check_uploaded_files_thread.is_alive() + ): + self.check_uploaded_files_stop_event.set() + self.check_uploaded_files_thread.join() + + # Join the tagger processing thread if running + if ( + self.process_tagger_queue_thread + and self.process_tagger_queue_thread.is_alive() + ): + self.process_tagger_queue_stop_event.set() + self.process_tagger_queue_thread.join() + + # Process any remaining main thread tasks + self.process_main_thread_queue() + + def join_check_uploaded_files_thread(self): + if self.check_uploaded_files_thread is not None: + self.check_uploaded_files_stop_event.set() + self.check_uploaded_files_thread.join() + self.check_uploaded_files_thread = None + self.check_uploaded_files_stop_event = threading.Event() + + def join_process_tagger_queue_thread(self): + if self.process_tagger_queue_thread is not None: + self.process_tagger_queue_stop_event.set() + self.process_tagger_queue_thread.join() + self.process_tagger_queue_thread = None + self.process_tagger_queue_stop_event = threading.Event() + + def process_tagger_for_image(self, file_path): + """Przetwarza obrazek przy użyciu Taggera i zapisuje wynik do cache.""" + result = self.get_tagger_results(file_path) + if self.process_tagger_for_image_callback: + self.process_tagger_for_image_callback(file_path, result.rating) + + def process_tagger_queue(self): + """Przetwarza wszystkie obrazki w tle (pomijając aktualnie wybrany).""" + for file_path in self.image_files: + if self.process_tagger_queue_stop_event.is_set(): + break + # Jeśli obrazek jest aktualnie wybrany, pomijamy – on będzie przetwarzany w foreground + if ( + self.current_index is not None + and file_path == self.image_files[self.current_index] + ): + continue + self.process_tagger_for_image(file_path) + self.schedule_in_main_thread(self.join_process_tagger_queue_thread, 100) + + def check_uploaded_files(self): + """ + Dla każdego obrazu oblicza MD5, grupuje je w paczki (do 100 skrótów), + wysyła zapytanie do endpointa 'posts.json' dla każdej paczki, + a następnie na podstawie odpowiedzi ustawia w self.uploaded post id dla uploadowanych plików. + """ + file_md5_list = [ + (idx, file, self.image_files_md5[file]) + for idx, file in enumerate(self.image_files) + ] + + batch_size = 100 + for i in range(0, len(file_md5_list), batch_size): + if self.check_uploaded_files_stop_event.is_set(): + break + batch = file_md5_list[i : i + batch_size] + batch_md5 = [item[2] for item in batch] + md5_param = ",".join(batch_md5) + url = self.settings.base_url.rstrip("/") + "/posts.json" + try: + response = requests.get(url, params={"md5": md5_param}) + + root = response.json() + found = {} + for elem in root: + if self.check_uploaded_files_stop_event.is_set(): + break + post_md5 = elem.get("md5", "").lower() + post_id = elem.get("id") + if post_md5 and post_id: + found[post_md5] = post_id + for idx, file_path, md5 in batch: + if self.check_uploaded_files_stop_event.is_set(): + break + self.upload_verified += 1 # Każdy plik w batchu jest zweryfikowany + if md5.lower() in found: + self.uploaded[file_path] = found[md5.lower()] + self.uploaded_count += 1 + if self.check_uploaded_files_callback: + self.check_uploaded_files_callback(idx) + else: + self.uploaded[file_path] = False + if self.update_status_bar_callback: + self.update_status_bar_callback() + + except Exception as e: + print(_("Błąd podczas sprawdzania paczki uploadu:"), e) + self.schedule_in_main_thread(self.join_check_uploaded_files_thread, 100) + + def autotag_files(self, file_paths): + """ + Autotaguje pliki przy użyciu Taggera i wysyła je na serwer. + """ + for file_path in file_paths: + if not os.path.isfile(file_path): + print(_("Plik nie istnieje:"), file_path) + continue + try: + tags, rating = self.compute_final_tags_and_rating_for_file( + file_path, lambda: None + ) + print(_("Tagi dla pliku"), file_path, ":", tags, "Rating:", rating) + self.upload_file( + file_path, + final_tags=tags, + final_rating=rating, + progress_queue=None, + cancel_event=None, + ) + except Exception as e: + print(_("Błąd podczas autotagowania pliku"), file_path, ":", e) + + def autotag_dir(self, dir): + """ + Autotaguje wszystkie pliki w katalogu przy użyciu Taggera i wysyła je na serwer. + """ + if not os.path.isdir(dir): + print(_("Podana ścieżka nie jest katalogiem:"), dir) + return + self.folder_path = dir + self.load_images() + self.check_uploaded_files_thread.join() + self.process_tagger_queue_thread.join() + files_to_upload = [x for x in self.image_files if not self.uploaded[x]] + if not files_to_upload: + print(_("Brak obrazów do przetworzenia w katalogu:"), dir) + return + + self.autotag_files(files_to_upload) + + def upload_file( + self, + file_path, + final_tags=None, + final_rating=None, + progress_queue: queue.Queue | None = None, + cancel_event: threading.Event | None = None, + info_callback: Callable[[str], Any] | None = None, + warning_callback: Callable[[str], Any] | None = None, + error_callback: Callable[[str], Any] | None = None, + ): + base_file_name = os.path.basename(file_path) + if progress_queue: + progress_queue.put(("mode", "determinate")) + progress_queue.put(("max", 100)) + progress_queue.put( + ( + "label", + _("Wysyłam plik {base_file_name}...").format( + base_file_name=base_file_name + ), + ) + ) + url = self.settings.base_url.rstrip("/") + "/api/danbooru/add_post" + tags = final_tags + fields = { + "login": self.settings.username, + "password": self.settings.password, + "tags": tags, + "source": "", + } + rating_value = self.rating_map.get(final_rating, "") + if rating_value: + fields["rating"] = rating_value + try: + total_size = os.path.getsize(file_path) + + def progress_callback(bytes_read, total_size): + if progress_queue: + percentage = int(bytes_read / total_size * 100) + progress_queue.put(("progress", percentage)) + + with open(file_path, "rb") as f: + wrapped_file = ProgressFile( + f, progress_callback, total_size, cancel_event + ) + files = {"file": (base_file_name, wrapped_file, "image/png")} + response = requests.post(url, data=fields, files=files) + if progress_queue: + progress_queue.put(("progress", 100)) + show_warn = False + post_url = None + if response.status_code in (200, 201): + message = _("Wysyłanie zakończone powodzeniem!") + post_url = response.headers.get("X-Danbooru-Location", None) + elif response.status_code == 409: + message = _( + "Wysyłanie zakończone błędem.\nStatus: {status_code}\nTreść: {text}" + ).format( + status_code=response.status_code, + text=response.headers.get("X-Danbooru-Errors", ""), + ) + post_url = response.headers.get("X-Danbooru-Location", None) + show_warn = True + else: + message = _( + "Wysyłanie zakończone błędem.\nStatus: {status_code}\nTreść: {text}" + ).format(status_code=response.status_code, text=response.text) + show_warn = True + # Aktualizacja wyglądu listy – musimy użyć domyślnych argumentów w lambdzie, aby zachować bieżący indeks + if show_warn: + if not final_tags: + if warning_callback: + warning_callback(message) + else: + print("[WARN]", _("Wysyłanie"), message) + else: + if not final_tags: + if info_callback: + info_callback(message) + else: + print("[INFO]", _("Wysyłanie"), message) + if self.upload_file_success_callback: + self.upload_file_success_callback(file_path) + if post_url: + post_id = post_url.split("/")[-1] + self.uploaded[file_path] = post_id + self.uploaded_count += 1 + self.after(0, self.update_status_bar) + except Exception as e: + if error_callback: + error_callback(str(e)) + else: + print("[ERROR]", _("Błąd wysyłania pliku"), file_path, ":", e) + finally: + if self.upload_file_completed_callback: + self.upload_file_completed_callback() + + def upload_all_files( + self, + progress_queue: queue.Queue = None, + cancel_event: threading.Event = None, + secondary_progress_queue: queue.Queue = None, + update_status_callback=None, + manual_tags: set = set(), + info_callback: Callable[[str], Any] | None = None, + warning_callback: Callable[[str], Any] | None = None, + error_callback: Callable[[str], Any] | None = None, + ): + files_to_upload = [x for x in self.image_files if not self.uploaded[x]] + files_count = len(files_to_upload) + if progress_queue: + progress_queue.put(("mode", "determinate")) + progress_queue.put(("max", 100)) + file_idx = 0 + for file_path in files_to_upload: + if progress_queue: + progress_queue.put(("progress", file_idx * 100 / files_count)) + progress_queue.put( + ("label", f"Wysyłam plik {file_idx+1}/{files_count}...") + ) + if cancel_event is not None and cancel_event.is_set(): + if progress_queue: + progress_queue.put(("label", _("Anulowano operację!"))) + return + if not self.uploaded.get(file_path, False): + final_tags, final_rating = self.compute_final_tags_and_rating_for_file( + file_path, + update_status_callback, # lambda: self.after(0, self.update_status_bar) + manual_tags, # set(self.manual_tags_manager.manual_tags) + ) + print( + _( + "Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" + ).format( + file_path=file_path, + final_tags=final_tags, + final_rating=final_rating, + ) + ) + self.upload_file( + file_path, + final_tags=final_tags, + final_rating=final_rating, + progress_queue=secondary_progress_queue, + cancel_event=cancel_event, + info_callback=info_callback, + warning_callback=warning_callback, + error_callback=error_callback, + ) + file_idx += 1 + if progress_queue: + progress_queue.put(("label", _("Przesłano pliki!"))) + progress_queue.put(("progress", 100)) + + def edit_file( + self, + file_path, + final_tags=None, + final_rating=None, + progress_queue=None, + cancel_event=None, + info_callback: Callable[[str], Any] | None = None, + error_callback: Callable[[str], Any] | None = None, + ): + """ + Update tags and rating for an existing post without uploading the file. + """ + base_file_name = os.path.basename(file_path) + post_id = self.uploaded.get(file_path) + + if not post_id: + if error_callback: + error_callback(_("Post nie został znaleziony dla tego pliku")) + else: + print( + "[ERROR]", + _("Post nie został znaleziony dla tego pliku"), + file_path, + ) + return + + if progress_queue: + progress_queue.put(("mode", "determinate")) + progress_queue.put(("max", 100)) + progress_queue.put( + ( + "label", + _("Aktualizuję tagi dla {base_file_name}...").format( + base_file_name=base_file_name + ), + ) + ) + + try: + # Check for cancellation before starting the operation. + if cancel_event is not None and cancel_event.is_set(): + if progress_queue: + progress_queue.put(("label", _("Operacja anulowana"))) + return + + # Get authentication session and token + session = login(self.settings) + auth_token = get_auth_token(session, self.settings) + + # Check cancellation after login if needed. + if cancel_event is not None and cancel_event.is_set(): + if progress_queue: + progress_queue.put(("label", _("Operacja anulowana"))) + return + + # Prepare tags and rating + tags = final_tags + rating_value = self.rating_map.get(final_rating, "?") + + # Prepare API request + url = self.settings.base_url.rstrip("/") + "/post/set" + payload = { + "auth_token": auth_token, + "image_id": post_id, + "title": base_file_name, + "owner": self.settings.username, + "tags": tags, + "source": "", + "rating": rating_value, + } + + if progress_queue: + progress_queue.put(("progress", 50)) + + # Check for cancellation before sending the update request. + if cancel_event is not None and cancel_event.is_set(): + if progress_queue: + progress_queue.put(("label", _("Operacja anulowana"))) + return + + # Send update request + response = session.post(url, data=payload, allow_redirects=False) + + # Handle 302 redirect as success case + if response.status_code == 302: + if progress_queue: + progress_queue.put(("progress", 100)) + message = _("Tagi zostały zaktualizowane!") + if not final_tags: # Only show success if not bulk operation + if info_callback: + info_callback(message) + else: + print("[INFO]", _("Sukces edycji"), message) + # Update UI state + if self.upload_file_completed_callback: + self.upload_file_completed_callback() + return + + # Handle other status codes + error_msg = _("Błąd podczas aktualizacji tagów\nStatus: {code}").format( + code=response.status_code + ) + if response.text: + error_msg += f"\n{_('Treść:')} {response.text}" + if error_callback: + error_callback(error_msg) + else: + print("[ERROR]", _("Błąd edycji"), error_msg) + + except Exception as e: + if error_callback: + error_callback(str(e)) + else: + print("[ERROR]", _("Krytyczny błąd edycji"), file_path, ":", e) + finally: + if progress_queue: + progress_queue.put(("progress", 100)) diff --git a/kapitanbooru_uploader/ImageBrowser.py b/kapitanbooru_uploader/ImageBrowser.py index 16eff76..870f650 100644 --- a/kapitanbooru_uploader/ImageBrowser.py +++ b/kapitanbooru_uploader/ImageBrowser.py @@ -1,12 +1,8 @@ -import glob -import hashlib 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 typing import Dict, Tuple from packaging.version import parse as parse_version import itertools @@ -18,13 +14,17 @@ import tomli from .ProcessingDialog import ProcessingDialog from .I18N import _ -from .ProgressFile import ProgressFile -from .TagsRepo import TagsRepo +from .Core import Core from .autocomplete import TagManager -from .common import get_auth_token, login, open_tag_wiki_url, open_webbrowser +from .common import open_tag_wiki_url, open_webbrowser from .settings import Settings -from .tag_processing import TAG_FIXES, extract_parameters, parse_parameters, process_tag, extract_artist_from_filename -from .tagger_cache import TaggerCache +from .tag_processing import ( + TAG_FIXES, + extract_parameters, + parse_parameters, + process_tag, + extract_artist_from_filename, +) class ImageBrowser(tk.Tk): @@ -32,74 +32,48 @@ class ImageBrowser(tk.Tk): super().__init__() self.title("Kapitanbooru Uploader") self.geometry("900x600") - self.version = "0.8.3" - self.acknowledged_version = parse_version(self.version) - - self.settings = Settings() - self.tags_repo = TagsRepo(self.settings) - - self.implication_graph = self.load_implication_graph() - self.missing_tags = set() # Track tags not in the graph - - # Dodatkowe ustawienia dla Taggera - self.tagger_name = "wdtagger" - self.tagger_version = ( - "1.0" # możesz ustawić wersję dynamicznie, jeśli to możliwe + self.core = Core(Settings()) + self.core.check_uploaded_files_callback = self.check_uploaded_files_callback + self.core.update_status_bar_callback = lambda: self.after( + 0, self.update_status_bar ) - self.tagger_cache = TaggerCache( - self.settings, self.tagger_name, self.tagger_version + self.core.process_tagger_for_image_callback = ( + self.update_listbox_item_color_by_rating + ) + self.core.upload_file_success_callback = lambda file_path: self.after( + 0, + lambda idx=self.core.image_files.index(file_path): self.listbox.itemconfig( + idx, {"bg": "lightgray"} + ), + ) + self.core.upload_file_completed_callback = lambda: self.upload_button.after( + 0, self.update_button_states ) - self.folder_path = "" - self.image_files = [] - self.image_files_md5 = [] - self.current_index = None - self.image_cache = None - self.tagger_thread_idx = 0 - self.tagger = wdt.Tagger() - - self.check_uploaded_files_stop_event = threading.Event() - self.check_uploaded_files_thread: Optional[threading.Thread] = None - self.process_tagger_queue_stop_event = threading.Event() - self.process_tagger_queue_thread: Optional[threading.Thread] = None self.run_tagger_threads: Dict[str, threading.Thread] = {} - # Liczniki statusu - self.total_files = 0 - self.tagger_processed = set() - self.upload_verified = 0 - self.uploaded_count = 0 - - # Oryginalny obraz (do skalowania) - self.current_image_original = None - self.current_parameters = "" - - # Mapa ratingów: wyświetlana nazwa -> wartość wysyłana - self.rating_map = { - "General": "g", - "Sensitive": "s", - "Questionable": "q", - "Explicit": "e", - "Unrated": "", - } - - # Słowniki przechowujące stany tagów (dla PNG i Taggera) - self.png_tags_states = {} - self.tagger_tags_states = {} - - # Ścieżki do ustawień i cache - - # Ładujemy ustawienia - - # Nowy słownik przechowujący informację, czy dany plik (ścieżka) został już uploadowany - self.uploaded = {} # key: file path, value: True/False - self.create_menu() self.create_widgets() self.bind_events() # Schedule first update check self.after(1000, self._schedule_update_check) + self.after(100, self.process_core_tasks) + + def process_core_tasks(self): + """Process Core's main thread tasks""" + self.core.process_main_thread_queue() + self.after(100, self.process_core_tasks) + + def check_uploaded_files_callback(self, idx): + """Callback for when a file's upload status is checked""" + self.after( + 0, + lambda idx=idx: self.listbox.itemconfig(idx, {"bg": "lightgray"}), + ) + # Jeśli aktualnie wybrany plik, zmień przycisk + if self.current_index == idx: + self.after(0, self.update_button_states) def _schedule_update_check(self): """Schedule periodic update checks""" @@ -126,14 +100,14 @@ class ImageBrowser(tk.Tk): remote_toml = tomli.loads(response.text) remote_version_str = remote_toml["project"]["version"] remote_version = parse_version(remote_version_str) - current_version = parse_version(self.version) + current_version = parse_version(self.core.version) if ( remote_version > current_version - and remote_version > self.acknowledged_version + and remote_version > self.core.acknowledged_version ): self.after(0, lambda: self._notify_user(remote_version_str)) - self.acknowledged_version = remote_version + self.core.acknowledged_version = remote_version except requests.exceptions.RequestException as e: print(_("Update check failed: {error}").format(error=e)) except KeyError as e: @@ -156,7 +130,7 @@ class ImageBrowser(tk.Tk): formatted_message = message_template.format( new_version=new_version, - current_version=self.version, + current_version=self.core.version, update_command=( "pipx upgrade kapitanbooru-uploader" if self.is_installed_via_pipx() @@ -175,43 +149,6 @@ class ImageBrowser(tk.Tk): self.create_menu() self.create_widgets() - def load_implication_graph(self) -> nx.DiGraph: - G = nx.DiGraph() - conn = self.tags_repo.get_conn() - cursor = conn.cursor() - - # Step 1: Add all tags from the 'tags' table - cursor.execute( - """ - SELECT - CASE category - WHEN 1 THEN 'artist:' || name - WHEN 3 THEN 'copyright:' || name - WHEN 4 THEN 'character:' || name - WHEN 5 THEN 'meta:' || name - ELSE name - END AS prefixed_name - FROM tags - """ - ) - db_tags = {row[0] for row in cursor.fetchall()} - G.add_nodes_from(db_tags) - - # Step 2: Add nodes from implications (antecedents/consequents not in 'tags' table) - cursor.execute("SELECT antecedent, consequent FROM tag_closure") - edge_tags = set() - for ant, cons in cursor.fetchall(): - edge_tags.add(ant) - edge_tags.add(cons) - G.add_nodes_from(edge_tags - db_tags) # Add tags only in implications - - # Step 3: Add edges - cursor.execute("SELECT antecedent, consequent FROM tag_closure") - G.add_edges_from(cursor.fetchall()) - - conn.close() - return G - def adjust_text_widget_height(self, widget): """ Ustawia wysokość widgetu Text na liczbę linii w jego treści, @@ -235,97 +172,6 @@ class ImageBrowser(tk.Tk): ) widget.config(height=min(num_lines, max_lines) if num_lines > 4 else 4) - def compute_final_tags_and_rating_for_file(self, file_path): - """ - Oblicza finalną listę tagów dla danego pliku oraz rating. - Łączy tagi z: - - pliku (PNG): parsowane przez parse_parameters, - - Taggera (wynik z cache lub wyliczony na bieżąco), - - ustawień (default tags), - - manualnych tagów (z pola manual_tags_entry), - oraz dodaje tag "meta:auto_upload". - Zwraca finalny ciąg tagów oraz rating. - """ - # Pobierz tagi z pliku - try: - img = Image.open(file_path) - parameters = extract_parameters(img, file_path) - artist_tag = extract_artist_from_filename(file_path) - png_tags = set( - [ - x - for x in parse_parameters(parameters, self.tags_repo).split() - if process_tag(x, self.tags_repo)[1] - is not None # Ignoruj nieistniejące tagi - ] - ) - if artist_tag: - png_tags.add("artist:" + artist_tag.replace(" ", "_").replace("\\", "")) - img.close() - except Exception as e: - print(_("Błąd przy otwieraniu pliku"), file_path, ":", e) - png_tags = set() - - # Pobierz tagi z Taggera – sprawdzając cache - result = self.get_tagger_results(file_path) - tagger_tags = set() - rating = "Unrated" - tagger_tags.update( - ( - TAG_FIXES[tag] if tag in TAG_FIXES else tag - for tag in result.general_tag_data.keys() - ) - ) # Zamień nieprawidłowe tagi na poprawne - for t in result.character_tags: - full_tag = "character:" + t.replace(" ", "_").replace("\\", "") - # Zamień nieprawidłowe tagi na poprawne - if full_tag in TAG_FIXES: - full_tag = TAG_FIXES[full_tag] - tagger_tags.add(full_tag) - rating = self.map_tagger_rating(result) - - # Pobierz tagi z ustawień i manualne - default_tags = set(self.settings.default_tags.split()) - manual_tags = set(self.manual_tags_manager.manual_tags) - - # Finalna lista: suma wszystkich tagów - final_tags = default_tags.union(png_tags).union(tagger_tags).union(manual_tags) - final_tags.add("meta:auto_upload") - return " ".join(sorted(final_tags)), rating - - def get_tagger_results(self, file_path) -> wdt.Result: - md5 = self.image_files_md5[file_path] - cached = self.tagger_cache[md5] - if cached: - self.tagger_processed.add(md5) - return cached["result"] - try: - with Image.open(file_path) as img: - result = self.tagger.tag(img) - self.tagger_cache[md5] = result - self.tagger_processed.add(md5) - self.after(0, self.update_status_bar) - print(_("Tagger przetworzył:"), f"{file_path}") - return result - except Exception as e: - print(_("Błąd Taggera dla"), file_path, ":", e) - - def map_tagger_rating(self, result: wdt.Result) -> str: - """ - Mapuje rating z Taggera na wartość używaną w Kapitanbooru. - """ - if result.rating == "general": - new_rating = "General" - elif result.rating == "sensitive": - new_rating = "Sensitive" - elif result.rating == "questionable": - new_rating = "Questionable" - elif result.rating == "explicit": - new_rating = "Explicit" - else: - new_rating = "Unrated" - return new_rating - def create_menu(self): menubar = tk.Menu(self) self.config(menu=menubar) @@ -386,18 +232,18 @@ class ImageBrowser(tk.Tk): frame, text="Kapitanbooru Uploader", font=("TkDefaultFont", 16, "bold") ).grid(row=next(row_counter), column=0, sticky=tk.W) - current_version = parse_version(self.version) - if current_version < self.acknowledged_version: + current_version = parse_version(self.core.version) + if current_version < self.core.acknowledged_version: ttk.Label( frame, text=_("A new version {new_version} is available!").format( - new_version=self.acknowledged_version + new_version=self.core.acknowledged_version ), foreground="red", ).grid(row=next(row_counter), column=0, sticky=tk.W) content = [ - _("Current version: {version}").format(version=self.version), + _("Current version: {version}").format(version=self.core.version), "", _("A GUI application for uploading images to KapitanBooru."), _("Features include image upload, tag management, automatic"), @@ -423,7 +269,7 @@ class ImageBrowser(tk.Tk): repo_url = "https://git.mlesniak.pl/kapitan/kapitanbooru-uploader" repo = ttk.Label(repo_frame, text=repo_url, cursor="hand2", foreground="blue") repo.pack(side=tk.LEFT, padx=(5, 0)) - repo.bind("", lambda e: open_webbrowser(repo_url, self.settings)) + repo.bind("", lambda e: open_webbrowser(repo_url, self.core.settings)) # Website link website_frame = ttk.Frame(frame) @@ -435,17 +281,19 @@ class ImageBrowser(tk.Tk): ) website.pack(side=tk.LEFT, padx=(5, 0)) website.bind( - "", lambda e: open_webbrowser(website_url, self.settings) + "", lambda e: open_webbrowser(website_url, self.core.settings) ) # Close button ttk.Button(about, text=_("Close"), command=about.destroy).pack(pady=10) def regenerate_tags_db(self): - self.processing_dialog = ProcessingDialog(self, self.tags_repo.regenerate_db) + self.processing_dialog = ProcessingDialog( + self, self.core.tags_repo.regenerate_db + ) def clear_cache(self): - res, err = self.tagger_cache.clear_cache() + res, err = self.core.tagger_cache.clear_cache() if res: messagebox.showinfo(_("Cache"), _("Cache Taggera zostało wyczyszczone.")) else: @@ -464,37 +312,37 @@ class ImageBrowser(tk.Tk): lbl_login.pack(pady=(10, 0)) entry_login = tk.Entry(settings_window) entry_login.pack(pady=(0, 10), padx=10, fill="x") - entry_login.insert(0, self.settings.username) + entry_login.insert(0, self.core.settings.username) lbl_password = tk.Label(settings_window, text=_("Hasło:")) lbl_password.pack(pady=(10, 0)) entry_password = tk.Entry(settings_window, show="*") entry_password.pack(pady=(0, 10), padx=10, fill="x") - entry_password.insert(0, self.settings.password) + entry_password.insert(0, self.core.settings.password) lbl_base_url = tk.Label(settings_window, text=_("Base URL:")) lbl_base_url.pack(pady=(10, 0)) entry_base_url = tk.Entry(settings_window) entry_base_url.pack(pady=(0, 10), padx=10, fill="x") - entry_base_url.insert(0, self.settings.base_url) + entry_base_url.insert(0, self.core.settings.base_url) lbl_default_tags = tk.Label(settings_window, text=_("Default Tags:")) lbl_default_tags.pack(pady=(10, 0)) entry_default_tags = tk.Entry(settings_window) entry_default_tags.pack(pady=(0, 10), padx=10, fill="x") - entry_default_tags.insert(0, self.settings.default_tags) + entry_default_tags.insert(0, self.core.settings.default_tags) lbl_browser = tk.Label(settings_window, text=_("Browser:")) lbl_browser.pack(pady=(10, 0)) cb_browser = ttk.Combobox( settings_window, - values=list(self.settings.installed_browsers.keys()), + values=list(self.core.settings.installed_browsers.keys()), state="readonly", ) cb_browser.pack(pady=(0, 10), padx=10, fill="x") cb_browser.set( - self.settings.installed_browsers_reverse.get( - self.settings.browser, "Default" + self.core.settings.installed_browsers_reverse.get( + self.core.settings.browser, "Default" ) ) @@ -502,31 +350,35 @@ class ImageBrowser(tk.Tk): lbl_lang.pack(pady=(10, 0)) cb_lang = ttk.Combobox( settings_window, - values=list(self.settings.i18n.languages.values()), + values=list(self.core.settings.i18n.languages.values()), state="readonly", ) cb_lang.pack(pady=(0, 10), padx=10, fill="x") cb_lang.set( - self.settings.i18n.languages.get(self.settings.i18n.current_lang, "en") + self.core.settings.i18n.languages.get( + self.core.settings.i18n.current_lang, "en" + ) ) def save_and_close(): - self.settings.username = entry_login.get() - self.settings.password = entry_password.get() - self.settings.base_url = entry_base_url.get() - self.settings.default_tags = entry_default_tags.get() - self.settings.browser = self.settings.installed_browsers[cb_browser.get()] - self.settings.i18n.set_language( + self.core.settings.username = entry_login.get() + self.core.settings.password = entry_password.get() + self.core.settings.base_url = entry_base_url.get() + self.core.settings.default_tags = entry_default_tags.get() + self.core.settings.browser = self.core.settings.installed_browsers[ + cb_browser.get() + ] + self.core.settings.i18n.set_language( next( ( lang - for lang, name in self.settings.i18n.languages.items() + for lang, name in self.core.settings.i18n.languages.items() if name == cb_lang.get() ), "en", ) ) - self.settings.save_settings() + self.core.settings.save_settings() settings_window.destroy() self.reload_ui() @@ -601,7 +453,10 @@ class ImageBrowser(tk.Tk): manual_frame = tk.LabelFrame(right_frame, text=_("Manual Tags")) manual_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=5, pady=5) self.manual_tags_manager = TagManager( - manual_frame, self.settings, self.tags_repo, self.update_final_tags + manual_frame, + self.core.settings, + self.core.tags_repo, + self.update_final_tags, ) self.manual_tags_manager.pack(fill=tk.BOTH, expand=True) @@ -648,9 +503,9 @@ class ImageBrowser(tk.Tk): def update_status_bar(self): status_text = ( - f"{_('Przetworzono tagi:')} {len(self.tagger_processed)}/{self.total_files} {_('plików')} | " - f"{_('Zweryfikowano status uploadu:')} {self.upload_verified}/{self.total_files} {_('plików')} | " - f"{_('Zuploadowano:')} {self.uploaded_count}/{self.upload_verified} {_('plików')}" + f"{_('Przetworzono tagi:')} {len(self.core.tagger_processed)}/{self.core.total_files} {_('plików')} | " + f"{_('Zweryfikowano status uploadu:')} {self.core.upload_verified}/{self.core.total_files} {_('plików')} | " + f"{_('Zuploadowano:')} {self.core.uploaded_count}/{self.core.upload_verified} {_('plików')}" ) self.status_label.config(text=status_text) @@ -682,131 +537,22 @@ class ImageBrowser(tk.Tk): 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, JPEG, WebP, AVIF i GIF z wybranego folderu. - """ - 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: 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 - self.uploaded_count = 0 - for file in self.image_files: - self.listbox.insert(tk.END, os.path.basename(file)) - self.uploaded[file] = False - if self.image_files: - self.current_index = 0 - self.listbox.select_set(0) - self.show_image(0) - self.post_load_processing() - else: - messagebox.showinfo( - _("Informacja"), - _("Brak plików PNG, JPEG, WebP, AVIF lub GIF w wybranym folderze."), - ) - - def post_load_processing(self): - """ - Po załadowaniu plików, sprawdza czy są jakieś pliki do uploadu oraz przetwarza Taggerem pliki. - """ - self.join_check_uploaded_files_thread() - self.check_uploaded_files_thread = threading.Thread( - target=self.check_uploaded_files - ) - self.check_uploaded_files_thread.start() - self.join_process_tagger_queue_thread() - self.process_tagger_queue_thread = threading.Thread( - target=self.process_tagger_queue - ) - self.process_tagger_queue_thread.start() - - def join_check_uploaded_files_thread(self): - if self.check_uploaded_files_thread is not None: - self.check_uploaded_files_stop_event.set() - self.check_uploaded_files_thread.join() - self.check_uploaded_files_thread = None - self.check_uploaded_files_stop_event = threading.Event() - - def join_process_tagger_queue_thread(self): - if self.process_tagger_queue_thread is not None: - self.process_tagger_queue_stop_event.set() - self.process_tagger_queue_thread.join() - self.process_tagger_queue_thread = None - self.process_tagger_queue_stop_event = threading.Event() - - def check_uploaded_files(self): - """ - Dla każdego obrazu oblicza MD5, grupuje je w paczki (do 100 skrótów), - wysyła zapytanie do endpointa 'posts.json' dla każdej paczki, - a następnie na podstawie odpowiedzi ustawia w self.uploaded post id dla uploadowanych plików. - """ - file_md5_list = [ - (idx, file, self.image_files_md5[file]) - for idx, file in enumerate(self.image_files) - ] - - batch_size = 100 - for i in range(0, len(file_md5_list), batch_size): - if self.check_uploaded_files_stop_event.is_set(): - break - batch = file_md5_list[i : i + batch_size] - batch_md5 = [item[2] for item in batch] - md5_param = ",".join(batch_md5) - url = self.settings.base_url.rstrip("/") + "/posts.json" - try: - response = requests.get(url, params={"md5": md5_param}) - - root = response.json() - found = {} - for elem in root: - if self.check_uploaded_files_stop_event.is_set(): - break - post_md5 = elem.get("md5", "").lower() - post_id = elem.get("id") - if post_md5 and post_id: - found[post_md5] = post_id - for idx, file_path, md5 in batch: - if self.check_uploaded_files_stop_event.is_set(): - break - self.upload_verified += 1 # Każdy plik w batchu jest zweryfikowany - if md5.lower() in found: - self.uploaded[file_path] = found[md5.lower()] - self.uploaded_count += 1 - self.after( - 0, - lambda idx=idx: self.listbox.itemconfig( - idx, {"bg": "lightgray"} - ), - ) - # Jeśli aktualnie wybrany plik, zmień przycisk - if self.current_index == idx: - self.after(0, self.update_button_states) - else: - self.uploaded[file_path] = False - self.after(0, self.update_status_bar) - except Exception as e: - print(_("Błąd podczas sprawdzania paczki uploadu:"), e) - self.after(100, self.join_check_uploaded_files_thread) + self.core.folder_path = folder + self.core.load_images() + # Clear the entire listbox to prepare for reloading new items + self.listbox.delete(0, tk.END) + for file in self.core.image_files: + self.listbox.insert(tk.END, os.path.basename(file)) + if self.core.image_files: + self.current_index = 0 + self.listbox.select_set(0) + self.show_image(0) + self.update_button_states() + else: + messagebox.showinfo( + _("Informacja"), + _("Brak plików PNG, JPEG, WebP, AVIF lub GIF w wybranym folderze."), + ) def update_button_states(self): """ @@ -836,7 +582,7 @@ class ImageBrowser(tk.Tk): """ has_current = self.current_index is not None post_id = ( - self.uploaded.get(self.image_files[self.current_index]) + self.core.uploaded.get(self.core.image_files[self.current_index]) if has_current else None ) @@ -868,27 +614,10 @@ class ImageBrowser(tk.Tk): """Otwiera w przeglądarce URL posta.""" if self.current_index is None: return - post_id = self.uploaded.get(self.image_files[self.current_index]) + post_id = self.core.uploaded.get(self.core.image_files[self.current_index]) if post_id: - url = self.settings.base_url.rstrip("/") + "/post/view/" + str(post_id) - open_webbrowser(url, self.settings) - - def compute_md5(self, file_path, chunk_size=8192): - """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(_("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)) + url = self.core.settings.base_url.rstrip("/") + "/post/view/" + str(post_id) + open_webbrowser(url, self.core.settings) def on_listbox_select(self, event): """ @@ -908,22 +637,41 @@ class ImageBrowser(tk.Tk): Wysyła POST z obrazkiem, tagami i ratingiem. Po zakończeniu uploadu, ustawia przycisk na "Wyświetl". """ - file_path = self.image_files[self.current_index] - if self.uploaded.get(file_path, False): + file_path = self.core.image_files[self.current_index] + if self.core.uploaded.get(file_path, False): # Jeśli plik już uploadowany, ustaw przycisk na "Wyświetl" self.update_button_states() return self.upload_button.config(state=tk.DISABLED) - self.processing_dialog = ProcessingDialog(self, self.upload_file, file_path) + self.processing_dialog = ProcessingDialog( + self, + lambda file_path, process_queue=None, cancel_event=None: self.core.upload_file( + file_path=file_path, + final_tags=self.final_tags_text.get("1.0", tk.END).strip(), + final_rating=self.rating_var.get(), + progress_queue=process_queue, + cancel_event=cancel_event, + warning_callback=lambda message: messagebox.showwarning( + _("Wysyłanie"), message + ), + info_callback=lambda message: messagebox.showinfo( + _("Wysyłanie"), message + ), + error_callback=lambda message: messagebox.showerror( + _("Błąd wysyłania"), message + ), + ), + file_path, + ) def show_image(self, index): """ Wyświetla obrazek o podanym indeksie z listy. Odczytuje tagi z pliku PNG, uruchamia Taggera, aktualizuje widgety z tagami. """ - if index < 0 or index >= len(self.image_files): + if index < 0 or index >= len(self.core.image_files): return - file_path = self.image_files[index] + file_path = self.core.image_files[index] try: img = Image.open(file_path) parameters = extract_parameters(img, file_path) @@ -931,9 +679,11 @@ class ImageBrowser(tk.Tk): self.current_image_original = img.copy() self.current_parameters = parameters self.update_display_image() - parsed_parameters = parse_parameters(parameters, self.tags_repo) + parsed_parameters = parse_parameters(parameters, self.core.tags_repo) if artist_tag: - parsed_parameters += f" artist:{artist_tag.replace(' ', '_').replace('\\', '')}" + parsed_parameters += ( + f" artist:{artist_tag.replace(' ', '_').replace('\\', '')}" + ) # Uaktualnij widget PNG Tags self.update_png_tags_widget(parsed_parameters.split()) # Uruchom Taggera w osobnym wątku @@ -950,12 +700,28 @@ class ImageBrowser(tk.Tk): Modyfikuje obrazek na serwerze. Wysyła POST z md5 obrazka, tagami i ratingiem. """ - file_path = self.image_files[self.current_index] - if not self.uploaded.get(file_path, False): + file_path = self.core.image_files[self.current_index] + if not self.core.uploaded.get(file_path, False): self.update_button_states() return self.upload_button.config(state=tk.DISABLED) - self.processing_dialog = ProcessingDialog(self, self.edit_file, file_path) + self.processing_dialog = ProcessingDialog( + self, + lambda file_path, progress_queue=None, cancel_event=None: self.core.edit_file( + file_path=file_path, + final_tags=self.final_tags_text.get("1.0", tk.END).strip(), + final_rating=self.rating_var.get(), + progress_queue=progress_queue, + cancel_event=cancel_event, + error_callback=lambda message: messagebox.showerror( + _("Błąd edycji"), message + ), + info_callback=lambda message: messagebox.showinfo( + _("Sukces edycji"), message + ), + ), + file_path, + ) def update_display_image(self): """ @@ -993,10 +759,10 @@ class ImageBrowser(tk.Tk): darkorange dla Questionable, red dla Explicit. Jeśli plik uploadowany, nie zmieniamy (pozostaje lightgray). """ # Jeśli plik jest oznaczony jako uploadowany, nic nie robimy - if self.uploaded.get(file_path, False): + if self.core.uploaded.get(file_path, False): return try: - index = self.image_files.index(file_path) + index = self.core.image_files.index(file_path) except ValueError: return # Mapowanie ratingu na kolor @@ -1029,7 +795,7 @@ class ImageBrowser(tk.Tk): return new_index = self.current_index - 1 if new_index < 0: - new_index = len(self.image_files) - 1 + new_index = len(self.core.image_files) - 1 self.current_index = new_index self.listbox.select_clear(0, tk.END) self.listbox.select_set(new_index) @@ -1044,7 +810,7 @@ class ImageBrowser(tk.Tk): if self.current_index is None: return new_index = self.current_index + 1 - if new_index >= len(self.image_files): + if new_index >= len(self.core.image_files): new_index = 0 self.current_index = new_index self.listbox.select_clear(0, tk.END) @@ -1149,25 +915,29 @@ class ImageBrowser(tk.Tk): # Safely collect implications from selected tags for tag in selected_tags: - if tag in self.implication_graph: - implied_by_selected.update(nx.descendants(self.implication_graph, tag)) + if tag in self.core.implication_graph: + implied_by_selected.update( + nx.descendants(self.core.implication_graph, tag) + ) else: print( _("Warning: Tag '{tag}' not found in implication graph").format( tag=tag ) ) - self.missing_tags.add(tag) # Log missing tags + self.core.missing_tags.add(tag) # Log missing tags for tag in selected_png_tags: - if tag in self.implication_graph: - implied_by_selected.update(nx.descendants(self.implication_graph, tag)) + if tag in self.core.implication_graph: + implied_by_selected.update( + nx.descendants(self.core.implication_graph, tag) + ) else: print( _("Warning: Tag '{tag}' not found in implication graph").format( tag=tag ) ) - self.missing_tags.add(tag) # Log missing tags + self.core.missing_tags.add(tag) # Log missing tags # Build visible list for tag, (selected, confidence) in self.tagger_tags_states.items(): @@ -1309,8 +1079,8 @@ class ImageBrowser(tk.Tk): - Normalny: np. niebieski, bez podkreślenia. """ final_set = set() - if self.settings.default_tags: - final_set.update(self.settings.default_tags.split()) + if self.core.settings.default_tags: + final_set.update(self.core.settings.default_tags.split()) for tag, selected in self.png_tags_states.items(): if selected: final_set.add(tag) @@ -1325,7 +1095,7 @@ class ImageBrowser(tk.Tk): self.final_tags_text.config(state=tk.NORMAL) self.final_tags_text.delete("1.0", tk.END) for tag in final_list: - tag, deprecated = process_tag(tag, self.tags_repo) + tag, deprecated = process_tag(tag, self.core.tags_repo) # Ustal kolor i podkreślenie na podstawie wyniku if deprecated is True: color = "red" @@ -1358,30 +1128,9 @@ class ImageBrowser(tk.Tk): for t in self.final_tags_text.tag_names(index): if t.startswith("final_"): actual_tag = t[len("final_") :] - open_tag_wiki_url(actual_tag, self.settings) + open_tag_wiki_url(actual_tag, self.core.settings) break - # --- Metody do cache'owania wyników Taggera --- - - def process_tagger_for_image(self, file_path): - """Przetwarza obrazek przy użyciu Taggera i zapisuje wynik do cache.""" - result = self.get_tagger_results(file_path) - self.update_listbox_item_color_by_rating(file_path, result.rating) - - def process_tagger_queue(self): - """Przetwarza wszystkie obrazki w tle (pomijając aktualnie wybrany).""" - for file_path in self.image_files: - if self.process_tagger_queue_stop_event.is_set(): - break - # Jeśli obrazek jest aktualnie wybrany, pomijamy – on będzie przetwarzany w foreground - if ( - self.current_index is not None - and file_path == self.image_files[self.current_index] - ): - continue - self.process_tagger_for_image(file_path) - self.after(100, self.join_process_tagger_queue_thread) - def run_tagger(self, thread_name): """ Przetwarza aktualnie wybrany obrazek. @@ -1395,217 +1144,14 @@ class ImageBrowser(tk.Tk): self.tagger_tags_text.delete("1.0", tk.END) self.tagger_tags_text.insert("1.0", _("Tagger przetwarza...")) self.tagger_tags_text.config(state=tk.DISABLED) - file_path = self.image_files[self.current_index] - result = self.get_tagger_results(file_path) - new_rating = self.map_tagger_rating(result) + file_path = self.core.image_files[self.current_index] + result = self.core.get_tagger_results(file_path) + new_rating = self.core.map_tagger_rating(result) self.rating_var.set(new_rating) self.after(0, lambda: self.update_tagger_tags_widget(result)) self.update_listbox_item_color_by_rating(file_path, result.rating) self.after(100, lambda: self.run_tagger_threads[thread_name].join()) - def upload_file( - self, - file_path, - final_tags=None, - final_rating=None, - progress_queue: Optional[queue.Queue] = None, - cancel_event: Optional[threading.Event] = None, - ): - base_file_name = os.path.basename(file_path) - if progress_queue: - progress_queue.put(("mode", "determinate")) - progress_queue.put(("max", 100)) - progress_queue.put( - ( - "label", - _("Wysyłam plik {base_file_name}...").format( - base_file_name=base_file_name - ), - ) - ) - url = self.settings.base_url.rstrip("/") + "/api/danbooru/add_post" - tags = ( - self.final_tags_text.get("1.0", tk.END).strip() - if final_tags is None - else final_tags - ) - fields = { - "login": self.settings.username, - "password": self.settings.password, - "tags": tags, - "source": "", - } - rating_value = self.rating_map.get( - self.rating_var.get() if final_rating is None else final_rating, "" - ) - if rating_value: - fields["rating"] = rating_value - try: - total_size = os.path.getsize(file_path) - - def progress_callback(bytes_read, total_size): - if progress_queue: - percentage = int(bytes_read / total_size * 100) - progress_queue.put(("progress", percentage)) - - with open(file_path, "rb") as f: - wrapped_file = ProgressFile( - f, progress_callback, total_size, cancel_event - ) - files = {"file": (base_file_name, wrapped_file, "image/png")} - response = requests.post(url, data=fields, files=files) - if progress_queue: - progress_queue.put(("progress", 100)) - show_warn = False - post_url = None - if response.status_code in (200, 201): - message = _("Wysyłanie zakończone powodzeniem!") - post_url = response.headers.get("X-Danbooru-Location", None) - elif response.status_code == 409: - message = _( - "Wysyłanie zakończone błędem.\nStatus: {status_code}\nTreść: {text}" - ).format( - status_code=response.status_code, - text=response.headers.get("X-Danbooru-Errors", ""), - ) - post_url = response.headers.get("X-Danbooru-Location", None) - show_warn = True - else: - message = _( - "Wysyłanie zakończone błędem.\nStatus: {status_code}\nTreść: {text}" - ).format(status_code=response.status_code, text=response.text) - show_warn = True - # Aktualizacja wyglądu listy – musimy użyć domyślnych argumentów w lambdzie, aby zachować bieżący indeks - if show_warn: - if not final_tags: - messagebox.showwarning(_("Wysyłanie"), message) - else: - if not final_tags: - messagebox.showinfo(_("Wysyłanie"), message) - self.after( - 0, - lambda idx=self.image_files.index( - file_path - ): self.listbox.itemconfig(idx, {"bg": "lightgray"}), - ) - if post_url: - post_id = post_url.split("/")[-1] - self.uploaded[file_path] = post_id - self.uploaded_count += 1 - self.after(0, self.update_status_bar) - except Exception as e: - messagebox.showerror(_("Błąd wysyłania"), str(e)) - finally: - self.upload_button.after(0, self.update_button_states) - - def edit_file( - self, - file_path, - final_tags=None, - final_rating=None, - progress_queue=None, - cancel_event=None, - ): - """ - Update tags and rating for an existing post without uploading the file. - """ - base_file_name = os.path.basename(file_path) - post_id = self.uploaded.get(file_path) - - if not post_id: - messagebox.showerror( - _("Błąd edycji"), _("Post nie został znaleziony dla tego pliku") - ) - return - - if progress_queue: - progress_queue.put(("mode", "determinate")) - progress_queue.put(("max", 100)) - progress_queue.put( - ( - "label", - _("Aktualizuję tagi dla {base_file_name}...").format( - base_file_name=base_file_name - ), - ) - ) - - try: - # Check for cancellation before starting the operation. - if cancel_event is not None and cancel_event.is_set(): - if progress_queue: - progress_queue.put(("label", _("Operacja anulowana"))) - return - - # Get authentication session and token - session = login(self.settings) - auth_token = get_auth_token(session, self.settings) - - # Check cancellation after login if needed. - if cancel_event is not None and cancel_event.is_set(): - if progress_queue: - progress_queue.put(("label", _("Operacja anulowana"))) - return - - # Prepare tags and rating - tags = ( - self.final_tags_text.get("1.0", tk.END).strip() - if final_tags is None - else final_tags - ) - rating_value = self.rating_map.get( - self.rating_var.get() if final_rating is None else final_rating, "?" - ) - - # Prepare API request - url = self.settings.base_url.rstrip("/") + "/post/set" - payload = { - "auth_token": auth_token, - "image_id": post_id, - "title": base_file_name, - "owner": self.settings.username, - "tags": tags, - "source": "", - "rating": rating_value, - } - - if progress_queue: - progress_queue.put(("progress", 50)) - - # Check for cancellation before sending the update request. - if cancel_event is not None and cancel_event.is_set(): - if progress_queue: - progress_queue.put(("label", _("Operacja anulowana"))) - return - - # Send update request - response = session.post(url, data=payload, allow_redirects=False) - - # Handle 302 redirect as success case - if response.status_code == 302: - if progress_queue: - progress_queue.put(("progress", 100)) - message = _("Tagi zostały zaktualizowane!") - if not final_tags: # Only show success if not bulk operation - messagebox.showinfo(_("Sukces edycji"), message) - # Update UI state - self.after(0, self.update_button_states) - return - - # Handle other status codes - error_msg = _("Błąd podczas aktualizacji tagów\nStatus: {code}").format( - code=response.status_code - ) - if response.text: - error_msg += f"\n{_('Treść:')} {response.text}" - messagebox.showerror("Błąd edycji", error_msg) - - except Exception as e: - messagebox.showerror(_("Krytyczny błąd edycji"), str(e)) - finally: - if progress_queue: - progress_queue.put(("progress", 100)) - def upload_all_files(self): """ Metoda, która po potwierdzeniu przez użytkownika uploaduje wszystkie niewrzucone pliki. @@ -1620,50 +1166,22 @@ class ImageBrowser(tk.Tk): ): return - def worker( - progress_queue: queue.Queue = None, - cancel_event: threading.Event = None, - secondary_progress_queue: queue.Queue = None, - ): - files_to_upload = [x for x in self.image_files if not self.uploaded[x]] - files_count = len(files_to_upload) - if progress_queue: - progress_queue.put(("mode", "determinate")) - progress_queue.put(("max", 100)) - file_idx = 0 - for file_path in files_to_upload: - if progress_queue: - progress_queue.put(("progress", file_idx * 100 / files_count)) - progress_queue.put( - ("label", f"Wysyłam plik {file_idx+1}/{files_count}...") - ) - if cancel_event is not None and cancel_event.is_set(): - if progress_queue: - progress_queue.put(("label", _("Anulowano operację!"))) - return - if not self.uploaded.get(file_path, False): - final_tags, final_rating = ( - self.compute_final_tags_and_rating_for_file(file_path) - ) - print( - _( - "Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" - ).format( - file_path=file_path, - final_tags=final_tags, - final_rating=final_rating, - ) - ) - self.upload_file( - file_path, - final_tags=final_tags, - final_rating=final_rating, - progress_queue=secondary_progress_queue, - cancel_event=cancel_event, - ) - file_idx += 1 - if progress_queue: - progress_queue.put(("label", _("Przesłano pliki!"))) - progress_queue.put(("progress", 100)) - - self.processing_dialog = ProcessingDialog(self, worker) + self.processing_dialog = ProcessingDialog( + self, + lambda progress_queue=None, cancel_event=None, secondary_progress_queue=None: self.core.upload_all_files( + progress_queue=progress_queue, + cancel_event=cancel_event, + secondary_progress_queue=secondary_progress_queue, + update_status_callback=lambda: self.after(0, self.update_status_bar), + manual_tags=set(self.manual_tags_manager.manual_tags), + warning_callback=lambda message: messagebox.showwarning( + _("Wysyłanie"), message + ), + info_callback=lambda message: messagebox.showinfo( + _("Wysyłanie"), message + ), + error_callback=lambda message: messagebox.showerror( + _("Błąd wysyłania"), message + ), + ), + ) diff --git a/kapitanbooru_uploader/__main__.py b/kapitanbooru_uploader/__main__.py index 0252ad3..0377ab9 100644 --- a/kapitanbooru_uploader/__main__.py +++ b/kapitanbooru_uploader/__main__.py @@ -1,11 +1,43 @@ """kapitanbooru_uploader.__main__: executed when kapitanbooru_uploader directory is called as script.""" + +from .Core import Core +from .settings import Settings from .ImageBrowser import ImageBrowser +import argparse def main(): - app = ImageBrowser() - app.mainloop() + parser = argparse.ArgumentParser( + prog="kapitanbooru-uploader", description="KapitanBooru Uploader" + ) + group = parser.add_mutually_exclusive_group() + + # Add arguments to the group + group.add_argument( + "--autotag-files", + nargs="+", + metavar="PATH", + help="Specify one or more files to process", + ) + group.add_argument( + "--autotag-dir", metavar="PATH", help="Specify a directory to process" + ) + + args = parser.parse_args() + + # Determine which function to call + if args.autotag_files: + core = Core(Settings(), gui_mode=False) + core.autotag_files(args.autotag_files) + core.wait_for_completion() # Wait for threads to finish + elif args.autotag_dir: + core = Core(Settings(), gui_mode=False) + core.autotag_dir(args.autotag_dir) + core.wait_for_completion() # Wait for threads to finish + else: + app = ImageBrowser() + app.mainloop() if __name__ == "__main__": diff --git a/kapitanbooru_uploader/locales/en/LC_MESSAGES/messages.po b/kapitanbooru_uploader/locales/en/LC_MESSAGES/messages.po index 86b8955..f659451 100644 --- a/kapitanbooru_uploader/locales/en/LC_MESSAGES/messages.po +++ b/kapitanbooru_uploader/locales/en/LC_MESSAGES/messages.po @@ -1,8 +1,8 @@ msgid "" msgstr "" -"Project-Id-Version: Kapitanbooru Uploader 0.8.3\n" +"Project-Id-Version: Kapitanbooru Uploader 0.9.0\n" "Report-Msgid-Bugs-To: kapitan@mlesniak.pl\n" -"POT-Creation-Date: 2025-06-25 23:49+0200\n" +"POT-Creation-Date: 2025-06-26 17:07+0200\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -43,273 +43,56 @@ msgstr "Found auth_token:" msgid "auth_token not found in the HTML page." msgstr "auth_token not found in the HTML page." -#: ImageBrowser.py:138 -#, python-brace-format -msgid "Update check failed: {error}" -msgstr "Update check failed: {error}" - -#: ImageBrowser.py:140 -#, python-brace-format -msgid "Malformed pyproject.toml: {error}" -msgstr "Malformed pyproject.toml: {error}" - -#: ImageBrowser.py:143 -#, python-brace-format -msgid "Unexpected error during update check: {error}" -msgstr "Unexpected error during update check: {error}" - -#: ImageBrowser.py:150 -msgid "Update Available" -msgstr "Update Available" - -#: ImageBrowser.py:152 -#, python-brace-format -msgid "" -"A new version {new_version} is available!\n" -"You have version {current_version}.\n" -"\n" -"Update using: {update_command}" -msgstr "" -"A new version {new_version} is available!\n" -"You have version {current_version}.\n" -"\n" -"Update using: {update_command}" - -#: ImageBrowser.py:266 +#: Core.py:204 msgid "Błąd przy otwieraniu pliku" msgstr "Error opening file" -#: ImageBrowser.py:308 +#: Core.py:245 msgid "Tagger przetworzył:" msgstr "Tagger processed:" -#: ImageBrowser.py:311 +#: Core.py:248 msgid "Błąd Taggera dla" msgstr "Tagger error for" -#: ImageBrowser.py:337 -msgid "Otwórz folder" -msgstr "Open folder" - -#: ImageBrowser.py:340 ImageBrowser.py:629 ImageBrowser.py:856 -#: ImageBrowser.py:864 -msgid "Wyślij" -msgstr "Send" - -#: ImageBrowser.py:343 ImageBrowser.py:849 -msgid "Wyślij wszystko" -msgstr "Send all" - -#: ImageBrowser.py:347 ImageBrowser.py:850 ImageBrowser.py:856 -msgid "Podmień tagi" -msgstr "Replace tags" - -#: ImageBrowser.py:350 ImageBrowser.py:851 -msgid "Otwórz post" -msgstr "Open post" - -#: ImageBrowser.py:353 -msgid "Zakończ" -msgstr "Finish" - -#: ImageBrowser.py:355 -msgid "Plik" -msgstr "File" - -#: ImageBrowser.py:359 ImageBrowser.py:458 -msgid "Ustawienia" -msgstr "Settings" - -#: ImageBrowser.py:362 -msgid "Wyczyść cache Taggera" -msgstr "Clear Tagger cache" - -#: ImageBrowser.py:365 -msgid "Zregeneruj bazę tagów" -msgstr "Regenerate tag database" - -#: ImageBrowser.py:367 -msgid "Opcje" -msgstr "Options" - -#: ImageBrowser.py:370 -msgid "About" -msgstr "About" - -#: ImageBrowser.py:371 -msgid "Help" -msgstr "Help" - -#: ImageBrowser.py:376 -msgid "About Kapitanbooru Uploader" -msgstr "About Kapitanbooru Uploader" - -#: ImageBrowser.py:393 -#, python-brace-format -msgid "A new version {new_version} is available!" -msgstr "A new version {new_version} is available!" - -#: ImageBrowser.py:400 -#, python-brace-format -msgid "Current version: {version}" -msgstr "Current version: {version}" - -#: ImageBrowser.py:402 -msgid "A GUI application for uploading images to KapitanBooru." -msgstr "A GUI application for uploading images to KapitanBooru." - -#: ImageBrowser.py:403 -msgid "Features include image upload, tag management, automatic" -msgstr "Features include image upload, tag management, automatic" - -#: ImageBrowser.py:404 -msgid "tagging with wdtagger, and cache management." -msgstr "tagging with wdtagger, and cache management." - -#: ImageBrowser.py:406 -msgid "Authors:" -msgstr "Authors:" - -#: ImageBrowser.py:409 -msgid "License: MIT License" -msgstr "License: MIT License" - -#: ImageBrowser.py:422 -msgid "Repository:" -msgstr "Repository:" - -#: ImageBrowser.py:431 -msgid "Website:" -msgstr "Website:" - -#: ImageBrowser.py:442 -msgid "Close" -msgstr "Close" - -#: ImageBrowser.py:450 ImageBrowser.py:453 -msgid "Cache" -msgstr "Cache" - -#: ImageBrowser.py:450 -msgid "Cache Taggera zostało wyczyszczone." -msgstr "Tagger cache has been cleared." - -#: ImageBrowser.py:453 -msgid "Błąd przy czyszczeniu cache:" -msgstr "Error clearing cache:" - -#: ImageBrowser.py:463 -msgid "Login:" -msgstr "Login:" - -#: ImageBrowser.py:469 -msgid "Hasło:" -msgstr "Password:" - -#: ImageBrowser.py:475 -msgid "Base URL:" -msgstr "Base URL:" - -#: ImageBrowser.py:481 -msgid "Default Tags:" -msgstr "Default Tags:" - -#: ImageBrowser.py:487 -msgid "Browser:" -msgstr "Browser:" - -#: ImageBrowser.py:501 -msgid "Language:" -msgstr "Language:" - -#: ImageBrowser.py:533 -msgid "Zapisz" -msgstr "Save" - -#: ImageBrowser.py:575 -msgid "PNG Tags" -msgstr "PNG Tags" - -#: ImageBrowser.py:587 -msgid "Tagger Tags" -msgstr "Tagger Tags" - -#: ImageBrowser.py:601 -msgid "Manual Tags" -msgstr "Manual Tags" - -#: ImageBrowser.py:609 -msgid "Final Tags" -msgstr "Final Tags" - -#: ImageBrowser.py:634 -msgid "Wyświetl" -msgstr "Display" - -#: ImageBrowser.py:651 -msgid "Przetworzono tagi:" -msgstr "Processed tags:" - -#: ImageBrowser.py:651 ImageBrowser.py:652 ImageBrowser.py:653 -msgid "plików" -msgstr "files" - -#: ImageBrowser.py:652 -msgid "Zweryfikowano status uploadu:" -msgstr "Upload status verified:" - -#: ImageBrowser.py:653 -msgid "Zuploadowano:" -msgstr "Uploaded:" - -#: ImageBrowser.py:682 -msgid "Wybierz folder z obrazkami PNG, JPEG, WebP, AVIF i GIF" -msgstr "Select folder with PNG, JPEG, WebP, AVIF, and GIF images" - -#: ImageBrowser.py:724 -msgid "Informacja" -msgstr "Information" - -#: ImageBrowser.py:725 -#, fuzzy -msgid "Brak plików PNG, JPEG, WebP, AVIF lub GIF w wybranym folderze." -msgstr "No PNG files in the selected folder." - -#: ImageBrowser.py:808 -msgid "Błąd podczas sprawdzania paczki uploadu:" -msgstr "Error while checking upload package:" - -#: ImageBrowser.py:884 +#: Core.py:303 msgid "Error computing MD5:" msgstr "Error computing MD5:" -#: ImageBrowser.py:946 -msgid "Błąd" -msgstr "Error" +#: Core.py:428 +msgid "Błąd podczas sprawdzania paczki uploadu:" +msgstr "Error while checking upload package:" -#: ImageBrowser.py:946 -msgid "Nie można załadować obrazka:" -msgstr "Unable to load image:" +#: Core.py:437 +msgid "Plik nie istnieje:" +msgstr "File does not exist:" -#: ImageBrowser.py:1156 ImageBrowser.py:1166 -#, python-brace-format -msgid "Warning: Tag '{tag}' not found in implication graph" -msgstr "Warning: Tag '{tag}' not found in implication graph" +#: Core.py:443 +msgid "Tagi dla pliku" +msgstr "Tags for file" -#: ImageBrowser.py:1396 -msgid "Tagger przetwarza..." -msgstr "Tagger processing..." +#: Core.py:452 +msgid "Błąd podczas autotagowania pliku" +msgstr "Error during file autotagowania" -#: ImageBrowser.py:1421 +#: Core.py:459 +msgid "Podana ścieżka nie jest katalogiem:" +msgstr "Given path is not a directory:" + +#: Core.py:467 +msgid "Brak obrazów do przetworzenia w katalogu:" +msgstr "No images to process in directory:" + +#: Core.py:490 #, python-brace-format msgid "Wysyłam plik {base_file_name}..." -msgstr "Sending file {base_file_name}..." +msgstr "Uploading file {base_file_name}..." -#: ImageBrowser.py:1462 +#: Core.py:525 msgid "Wysyłanie zakończone powodzeniem!" msgstr "Upload completed successfully!" -#: ImageBrowser.py:1466 ImageBrowser.py:1475 +#: Core.py:529 Core.py:538 #, python-brace-format msgid "" "Wysyłanie zakończone błędem.\n" @@ -320,40 +103,52 @@ msgstr "" "Status: {status_code}\n" "Content: {text}" -#: ImageBrowser.py:1481 ImageBrowser.py:1484 +#: Core.py:547 Core.py:553 ImageBrowser.py:654 ImageBrowser.py:657 +#: ImageBrowser.py:1177 ImageBrowser.py:1180 msgid "Wysyłanie" msgstr "Uploading" -#: ImageBrowser.py:1497 -msgid "Błąd wysyłania" +#: Core.py:565 +msgid "Błąd wysyłania pliku" msgstr "Upload error" -#: ImageBrowser.py:1517 -msgid "Błąd edycji" -msgstr "Edit error" +#: Core.py:595 +msgid "Anulowano operację!" +msgstr "Operation cancelled!" -#: ImageBrowser.py:1517 +#: Core.py:605 +#, python-brace-format +msgid "" +"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" +msgstr "" +"Uploading {file_path} with tags: {final_tags} and rating: {final_rating}" + +#: Core.py:624 +msgid "Przesłano pliki!" +msgstr "Files have been uploaded!" + +#: Core.py:645 Core.py:649 msgid "Post nie został znaleziony dla tego pliku" msgstr "Post not found for this file" -#: ImageBrowser.py:1527 +#: Core.py:660 #, python-brace-format msgid "Aktualizuję tagi dla {base_file_name}..." msgstr "Updating tags for {base_file_name}..." -#: ImageBrowser.py:1537 ImageBrowser.py:1547 ImageBrowser.py:1578 +#: Core.py:670 Core.py:680 Core.py:705 msgid "Operacja anulowana" msgstr "Operation cancelled" -#: ImageBrowser.py:1588 +#: Core.py:715 msgid "Tagi zostały zaktualizowane!" msgstr "Tags have been updated!" -#: ImageBrowser.py:1590 +#: Core.py:720 ImageBrowser.py:719 msgid "Sukces edycji" msgstr "Edit successful" -#: ImageBrowser.py:1596 +#: Core.py:727 #, python-brace-format msgid "" "Błąd podczas aktualizacji tagów\n" @@ -362,19 +157,263 @@ msgstr "" "Error updating tags\n" "Status: {code}" -#: ImageBrowser.py:1600 +#: Core.py:731 msgid "Treść:" msgstr "Content:" -#: ImageBrowser.py:1604 +#: Core.py:735 ImageBrowser.py:716 +msgid "Błąd edycji" +msgstr "Edit error" + +#: Core.py:741 msgid "Krytyczny błąd edycji" msgstr "Critical edit error" -#: ImageBrowser.py:1616 +#: ImageBrowser.py:112 +#, python-brace-format +msgid "Update check failed: {error}" +msgstr "Update check failed: {error}" + +#: ImageBrowser.py:114 +#, python-brace-format +msgid "Malformed pyproject.toml: {error}" +msgstr "Malformed pyproject.toml: {error}" + +#: ImageBrowser.py:117 +#, python-brace-format +msgid "Unexpected error during update check: {error}" +msgstr "Unexpected error during update check: {error}" + +#: ImageBrowser.py:124 +msgid "Update Available" +msgstr "Update Available" + +#: ImageBrowser.py:126 +#, python-brace-format +msgid "" +"A new version {new_version} is available!\n" +"You have version {current_version}.\n" +"\n" +"Update using: {update_command}" +msgstr "" +"A new version {new_version} is available!\n" +"You have version {current_version}.\n" +"\n" +"Update using: {update_command}" + +#: ImageBrowser.py:183 +msgid "Otwórz folder" +msgstr "Open folder" + +#: ImageBrowser.py:186 ImageBrowser.py:484 ImageBrowser.py:601 +#: ImageBrowser.py:609 +msgid "Wyślij" +msgstr "Upload" + +#: ImageBrowser.py:189 ImageBrowser.py:594 +msgid "Wyślij wszystko" +msgstr "Upload all" + +#: ImageBrowser.py:193 ImageBrowser.py:595 ImageBrowser.py:601 +msgid "Podmień tagi" +msgstr "Replace tags" + +#: ImageBrowser.py:196 ImageBrowser.py:596 +msgid "Otwórz post" +msgstr "Open post" + +#: ImageBrowser.py:199 +msgid "Zakończ" +msgstr "Finish" + +#: ImageBrowser.py:201 +msgid "Plik" +msgstr "File" + +#: ImageBrowser.py:205 ImageBrowser.py:306 +msgid "Ustawienia" +msgstr "Settings" + +#: ImageBrowser.py:208 +msgid "Wyczyść cache Taggera" +msgstr "Clear Tagger cache" + +#: ImageBrowser.py:211 +msgid "Zregeneruj bazę tagów" +msgstr "Regenerate tag database" + +#: ImageBrowser.py:213 +msgid "Opcje" +msgstr "Options" + +#: ImageBrowser.py:216 +msgid "About" +msgstr "About" + +#: ImageBrowser.py:217 +msgid "Help" +msgstr "Help" + +#: ImageBrowser.py:222 +msgid "About Kapitanbooru Uploader" +msgstr "About Kapitanbooru Uploader" + +#: ImageBrowser.py:239 +#, python-brace-format +msgid "A new version {new_version} is available!" +msgstr "A new version {new_version} is available!" + +#: ImageBrowser.py:246 +#, python-brace-format +msgid "Current version: {version}" +msgstr "Current version: {version}" + +#: ImageBrowser.py:248 +msgid "A GUI application for uploading images to KapitanBooru." +msgstr "A GUI application for uploading images to KapitanBooru." + +#: ImageBrowser.py:249 +msgid "Features include image upload, tag management, automatic" +msgstr "Features include image upload, tag management, automatic" + +#: ImageBrowser.py:250 +msgid "tagging with wdtagger, and cache management." +msgstr "tagging with wdtagger, and cache management." + +#: ImageBrowser.py:252 +msgid "Authors:" +msgstr "Authors:" + +#: ImageBrowser.py:255 +msgid "License: MIT License" +msgstr "License: MIT License" + +#: ImageBrowser.py:268 +msgid "Repository:" +msgstr "Repository:" + +#: ImageBrowser.py:277 +msgid "Website:" +msgstr "Website:" + +#: ImageBrowser.py:288 +msgid "Close" +msgstr "Close" + +#: ImageBrowser.py:298 ImageBrowser.py:301 +msgid "Cache" +msgstr "Cache" + +#: ImageBrowser.py:298 +msgid "Cache Taggera zostało wyczyszczone." +msgstr "Tagger cache has been cleared." + +#: ImageBrowser.py:301 +msgid "Błąd przy czyszczeniu cache:" +msgstr "Error clearing cache:" + +#: ImageBrowser.py:311 +msgid "Login:" +msgstr "Login:" + +#: ImageBrowser.py:317 +msgid "Hasło:" +msgstr "Password:" + +#: ImageBrowser.py:323 +msgid "Base URL:" +msgstr "Base URL:" + +#: ImageBrowser.py:329 +msgid "Default Tags:" +msgstr "Default Tags:" + +#: ImageBrowser.py:335 +msgid "Browser:" +msgstr "Browser:" + +#: ImageBrowser.py:349 +msgid "Language:" +msgstr "Language:" + +#: ImageBrowser.py:385 +msgid "Zapisz" +msgstr "Save" + +#: ImageBrowser.py:427 +msgid "PNG Tags" +msgstr "PNG Tags" + +#: ImageBrowser.py:439 +msgid "Tagger Tags" +msgstr "Tagger Tags" + +#: ImageBrowser.py:453 +msgid "Manual Tags" +msgstr "Manual Tags" + +#: ImageBrowser.py:464 +msgid "Final Tags" +msgstr "Final Tags" + +#: ImageBrowser.py:489 +msgid "Wyświetl" +msgstr "Display" + +#: ImageBrowser.py:506 +msgid "Przetworzono tagi:" +msgstr "Processed tags:" + +#: ImageBrowser.py:506 ImageBrowser.py:507 ImageBrowser.py:508 +msgid "plików" +msgstr "files" + +#: ImageBrowser.py:507 +msgid "Zweryfikowano status uploadu:" +msgstr "Upload status verified:" + +#: ImageBrowser.py:508 +msgid "Zuploadowano:" +msgstr "Uploaded:" + +#: ImageBrowser.py:537 +msgid "Wybierz folder z obrazkami PNG, JPEG, WebP, AVIF i GIF" +msgstr "Select folder with PNG, JPEG, WebP, AVIF, and GIF images" + +#: ImageBrowser.py:552 +msgid "Informacja" +msgstr "Information" + +#: ImageBrowser.py:553 +msgid "Brak plików PNG, JPEG, WebP, AVIF lub GIF w wybranym folderze." +msgstr "No PNG, JPEG, WebP, AVIF or GIF files in the selected folder." + +#: ImageBrowser.py:660 ImageBrowser.py:1183 +msgid "Błąd wysyłania" +msgstr "Upload error" + +#: ImageBrowser.py:695 +msgid "Błąd" +msgstr "Error" + +#: ImageBrowser.py:695 +msgid "Nie można załadować obrazka:" +msgstr "Unable to load image:" + +#: ImageBrowser.py:923 ImageBrowser.py:935 +#, python-brace-format +msgid "Warning: Tag '{tag}' not found in implication graph" +msgstr "Warning: Tag '{tag}' not found in implication graph" + +#: ImageBrowser.py:1144 +msgid "Tagger przetwarza..." +msgstr "Tagger processing..." + +#: ImageBrowser.py:1161 msgid "Potwierdzenie" msgstr "Confirmation" -#: ImageBrowser.py:1618 +#: ImageBrowser.py:1163 msgid "" "Czy na pewno chcesz wrzucić wszystkie niewrzucone pliki?\n" "Każdy z nich zostanie oznaczony tagiem 'meta:auto_upload'.\n" @@ -384,21 +423,6 @@ msgstr "" "Each will be tagged with 'meta:auto_upload'.\n" "Make sure the tags are correct!" -#: ImageBrowser.py:1642 -msgid "Anulowano operację!" -msgstr "Operation cancelled!" - -#: ImageBrowser.py:1650 -#, python-brace-format -msgid "" -"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" -msgstr "" -"Uploading {file_path} with tags: {final_tags} and rating: {final_rating}" - -#: ImageBrowser.py:1666 -msgid "Przesłano pliki!" -msgstr "Files have been uploaded!" - #: ProcessingDialog.py:15 msgid "Processing..." msgstr "Processing..." diff --git a/kapitanbooru_uploader/locales/pl/LC_MESSAGES/messages.po b/kapitanbooru_uploader/locales/pl/LC_MESSAGES/messages.po index 590dcc3..d154355 100644 --- a/kapitanbooru_uploader/locales/pl/LC_MESSAGES/messages.po +++ b/kapitanbooru_uploader/locales/pl/LC_MESSAGES/messages.po @@ -1,8 +1,8 @@ msgid "" msgstr "" -"Project-Id-Version: Kapitanbooru Uploader 0.8.3\n" +"Project-Id-Version: Kapitanbooru Uploader 0.9.0\n" "Report-Msgid-Bugs-To: kapitan@mlesniak.pl\n" -"POT-Creation-Date: 2025-06-25 23:49+0200\n" +"POT-Creation-Date: 2025-06-26 17:07+0200\n" "Language: pl\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -45,26 +45,152 @@ msgstr "Znaleziono auth_token:" msgid "auth_token not found in the HTML page." msgstr "Nie znaleziono auth_token w stronie HTML." -#: ImageBrowser.py:138 +#: Core.py:204 +msgid "Błąd przy otwieraniu pliku" +msgstr "Błąd przy otwieraniu pliku" + +#: Core.py:245 +msgid "Tagger przetworzył:" +msgstr "Tagger przetworzył:" + +#: Core.py:248 +msgid "Błąd Taggera dla" +msgstr "Błąd Taggera dla" + +#: Core.py:303 +msgid "Error computing MD5:" +msgstr "Błąd przy obliczaniu MD5:" + +#: Core.py:428 +msgid "Błąd podczas sprawdzania paczki uploadu:" +msgstr "Błąd podczas sprawdzania paczki uploadu:" + +#: Core.py:437 +msgid "Plik nie istnieje:" +msgstr "" + +#: Core.py:443 +msgid "Tagi dla pliku" +msgstr "" + +#: Core.py:452 +msgid "Błąd podczas autotagowania pliku" +msgstr "Błąd podczas autotagowania pliku" + +#: Core.py:459 +msgid "Podana ścieżka nie jest katalogiem:" +msgstr "" + +#: Core.py:467 +msgid "Brak obrazów do przetworzenia w katalogu:" +msgstr "" + +#: Core.py:490 +#, python-brace-format +msgid "Wysyłam plik {base_file_name}..." +msgstr "Wysyłam plik {base_file_name}..." + +#: Core.py:525 +msgid "Wysyłanie zakończone powodzeniem!" +msgstr "Wysyłanie zakończone powodzeniem!" + +#: Core.py:529 Core.py:538 +#, python-brace-format +msgid "" +"Wysyłanie zakończone błędem.\n" +"Status: {status_code}\n" +"Treść: {text}" +msgstr "" +"Wysyłanie zakończone błędem.\n" +"Status: {status_code}\n" +"Treść: {text}" + +#: Core.py:547 Core.py:553 ImageBrowser.py:654 ImageBrowser.py:657 +#: ImageBrowser.py:1177 ImageBrowser.py:1180 +msgid "Wysyłanie" +msgstr "Wysyłanie" + +#: Core.py:565 +msgid "Błąd wysyłania pliku" +msgstr "Błąd wysyłania pliku" + +#: Core.py:595 +msgid "Anulowano operację!" +msgstr "Operacja anulowana!" + +#: Core.py:605 +#, python-brace-format +msgid "" +"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" +msgstr "" +"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" + +#: Core.py:624 +msgid "Przesłano pliki!" +msgstr "Pliki zostały przesłane!" + +#: Core.py:645 Core.py:649 +msgid "Post nie został znaleziony dla tego pliku" +msgstr "Post nie został znaleziony dla tego pliku" + +#: Core.py:660 +#, python-brace-format +msgid "Aktualizuję tagi dla {base_file_name}..." +msgstr "Aktualizuję tagi dla {base_file_name}..." + +#: Core.py:670 Core.py:680 Core.py:705 +msgid "Operacja anulowana" +msgstr "Operacja anulowana" + +#: Core.py:715 +msgid "Tagi zostały zaktualizowane!" +msgstr "Tagi zostały zaktualizowane!" + +#: Core.py:720 ImageBrowser.py:719 +msgid "Sukces edycji" +msgstr "Sukces edycji" + +#: Core.py:727 +#, python-brace-format +msgid "" +"Błąd podczas aktualizacji tagów\n" +"Status: {code}" +msgstr "" +"Błąd podczas aktualizacji tagów\n" +"Status: {code}" + +#: Core.py:731 +msgid "Treść:" +msgstr "Treść:" + +#: Core.py:735 ImageBrowser.py:716 +msgid "Błąd edycji" +msgstr "Błąd edycji" + +#: Core.py:741 +msgid "Krytyczny błąd edycji" +msgstr "Krytyczny błąd edycji" + +#: ImageBrowser.py:112 #, python-brace-format msgid "Update check failed: {error}" msgstr "Sprawdzenie aktualizacji nie powiodło się: {error}" -#: ImageBrowser.py:140 +#: ImageBrowser.py:114 #, python-brace-format msgid "Malformed pyproject.toml: {error}" msgstr "Nieprawidłowy plik pyproject.toml: {error}" -#: ImageBrowser.py:143 +#: ImageBrowser.py:117 #, python-brace-format msgid "Unexpected error during update check: {error}" msgstr "Nieoczekiwany błąd podczas sprawdzania aktualizacji: {error}" -#: ImageBrowser.py:150 +#: ImageBrowser.py:124 msgid "Update Available" msgstr "Aktualizacja dostępna" -#: ImageBrowser.py:152 +#: ImageBrowser.py:126 #, python-brace-format msgid "" "A new version {new_version} is available!\n" @@ -77,307 +203,219 @@ msgstr "" "\n" "Aktualizuj za pomocą: {update_command}" -#: ImageBrowser.py:266 -msgid "Błąd przy otwieraniu pliku" -msgstr "Błąd przy otwieraniu pliku" - -#: ImageBrowser.py:308 -msgid "Tagger przetworzył:" -msgstr "Tagger przetworzył:" - -#: ImageBrowser.py:311 -msgid "Błąd Taggera dla" -msgstr "Błąd Taggera dla" - -#: ImageBrowser.py:337 +#: ImageBrowser.py:183 msgid "Otwórz folder" msgstr "Otwórz folder" -#: ImageBrowser.py:340 ImageBrowser.py:629 ImageBrowser.py:856 -#: ImageBrowser.py:864 +#: ImageBrowser.py:186 ImageBrowser.py:484 ImageBrowser.py:601 +#: ImageBrowser.py:609 msgid "Wyślij" msgstr "Wyślij" -#: ImageBrowser.py:343 ImageBrowser.py:849 +#: ImageBrowser.py:189 ImageBrowser.py:594 msgid "Wyślij wszystko" msgstr "Wyślij wszystko" -#: ImageBrowser.py:347 ImageBrowser.py:850 ImageBrowser.py:856 +#: ImageBrowser.py:193 ImageBrowser.py:595 ImageBrowser.py:601 msgid "Podmień tagi" msgstr "Podmień tagi" -#: ImageBrowser.py:350 ImageBrowser.py:851 +#: ImageBrowser.py:196 ImageBrowser.py:596 msgid "Otwórz post" msgstr "Otwórz post" -#: ImageBrowser.py:353 +#: ImageBrowser.py:199 msgid "Zakończ" msgstr "Zakończ" -#: ImageBrowser.py:355 +#: ImageBrowser.py:201 msgid "Plik" msgstr "Plik" -#: ImageBrowser.py:359 ImageBrowser.py:458 +#: ImageBrowser.py:205 ImageBrowser.py:306 msgid "Ustawienia" msgstr "Ustawienia" -#: ImageBrowser.py:362 +#: ImageBrowser.py:208 msgid "Wyczyść cache Taggera" msgstr "Wyczyść cache Taggera" -#: ImageBrowser.py:365 +#: ImageBrowser.py:211 msgid "Zregeneruj bazę tagów" msgstr "Zregeneruj bazę tagów" -#: ImageBrowser.py:367 +#: ImageBrowser.py:213 msgid "Opcje" msgstr "Opcje" -#: ImageBrowser.py:370 +#: ImageBrowser.py:216 msgid "About" msgstr "O programie" -#: ImageBrowser.py:371 +#: ImageBrowser.py:217 msgid "Help" msgstr "Pomoc" -#: ImageBrowser.py:376 +#: ImageBrowser.py:222 msgid "About Kapitanbooru Uploader" msgstr "O programie Kapitanbooru Uploader" -#: ImageBrowser.py:393 +#: ImageBrowser.py:239 #, python-brace-format msgid "A new version {new_version} is available!" msgstr "Dostępna jest nowa wersja {new_version}!" -#: ImageBrowser.py:400 +#: ImageBrowser.py:246 #, python-brace-format msgid "Current version: {version}" msgstr "Obecna wersja: {version}" -#: ImageBrowser.py:402 +#: ImageBrowser.py:248 msgid "A GUI application for uploading images to KapitanBooru." msgstr "Aplikacja GUI do przesyłania obrazów do KapitanBooru." -#: ImageBrowser.py:403 +#: ImageBrowser.py:249 msgid "Features include image upload, tag management, automatic" msgstr "Funkcje obejmują przesyłanie obrazów, zarządzanie tagami, automatyczne" -#: ImageBrowser.py:404 +#: ImageBrowser.py:250 msgid "tagging with wdtagger, and cache management." msgstr "tagowanie za pomocą wdtagger oraz zarządzanie cache." -#: ImageBrowser.py:406 +#: ImageBrowser.py:252 msgid "Authors:" msgstr "Autorzy:" -#: ImageBrowser.py:409 +#: ImageBrowser.py:255 msgid "License: MIT License" msgstr "Licencja: MIT License" -#: ImageBrowser.py:422 +#: ImageBrowser.py:268 msgid "Repository:" msgstr "Repozytorium:" -#: ImageBrowser.py:431 +#: ImageBrowser.py:277 msgid "Website:" msgstr "Strona internetowa:" -#: ImageBrowser.py:442 +#: ImageBrowser.py:288 msgid "Close" msgstr "Zamknij" -#: ImageBrowser.py:450 ImageBrowser.py:453 +#: ImageBrowser.py:298 ImageBrowser.py:301 msgid "Cache" msgstr "Cache" -#: ImageBrowser.py:450 +#: ImageBrowser.py:298 msgid "Cache Taggera zostało wyczyszczone." msgstr "Cache Taggera zostało wyczyszczone." -#: ImageBrowser.py:453 +#: ImageBrowser.py:301 msgid "Błąd przy czyszczeniu cache:" msgstr "Błąd przy czyszczeniu cache:" -#: ImageBrowser.py:463 +#: ImageBrowser.py:311 msgid "Login:" msgstr "Login:" -#: ImageBrowser.py:469 +#: ImageBrowser.py:317 msgid "Hasło:" msgstr "Hasło:" -#: ImageBrowser.py:475 +#: ImageBrowser.py:323 msgid "Base URL:" msgstr "Base URL:" -#: ImageBrowser.py:481 +#: ImageBrowser.py:329 msgid "Default Tags:" msgstr "Domyślne tagi:" -#: ImageBrowser.py:487 +#: ImageBrowser.py:335 msgid "Browser:" msgstr "Przeglądarka:" -#: ImageBrowser.py:501 +#: ImageBrowser.py:349 msgid "Language:" msgstr "Język:" -#: ImageBrowser.py:533 +#: ImageBrowser.py:385 msgid "Zapisz" msgstr "Zapisz" -#: ImageBrowser.py:575 +#: ImageBrowser.py:427 msgid "PNG Tags" msgstr "Tagi PNG" -#: ImageBrowser.py:587 +#: ImageBrowser.py:439 msgid "Tagger Tags" msgstr "Tagi Taggera" -#: ImageBrowser.py:601 +#: ImageBrowser.py:453 msgid "Manual Tags" msgstr "Tagi ręczne" -#: ImageBrowser.py:609 +#: ImageBrowser.py:464 msgid "Final Tags" msgstr "Ostateczne tagi" -#: ImageBrowser.py:634 +#: ImageBrowser.py:489 msgid "Wyświetl" msgstr "Wyświetl" -#: ImageBrowser.py:651 +#: ImageBrowser.py:506 msgid "Przetworzono tagi:" msgstr "Przetworzono tagi:" -#: ImageBrowser.py:651 ImageBrowser.py:652 ImageBrowser.py:653 +#: ImageBrowser.py:506 ImageBrowser.py:507 ImageBrowser.py:508 msgid "plików" msgstr "plików" -#: ImageBrowser.py:652 +#: ImageBrowser.py:507 msgid "Zweryfikowano status uploadu:" msgstr "Zweryfikowano status uploadu:" -#: ImageBrowser.py:653 +#: ImageBrowser.py:508 msgid "Zuploadowano:" msgstr "Zuploadowano:" -#: ImageBrowser.py:682 -#, fuzzy +#: ImageBrowser.py:537 msgid "Wybierz folder z obrazkami PNG, JPEG, WebP, AVIF i GIF" -msgstr "Wybierz folder z obrazami PNG" +msgstr "Wybierz folder z obrazkami PNG, JPEG, WebP, AVIF i GIF" -#: ImageBrowser.py:724 +#: ImageBrowser.py:552 msgid "Informacja" msgstr "Informacja" -#: ImageBrowser.py:725 -#, fuzzy +#: ImageBrowser.py:553 msgid "Brak plików PNG, JPEG, WebP, AVIF lub GIF w wybranym folderze." -msgstr "Brak plików PNG w wybranym folderze." +msgstr "Brak plików PNG, JPEG, WebP, AVIF lub GIF w wybranym folderze." -#: ImageBrowser.py:808 -msgid "Błąd podczas sprawdzania paczki uploadu:" -msgstr "Błąd podczas sprawdzania paczki uploadu:" +#: ImageBrowser.py:660 ImageBrowser.py:1183 +msgid "Błąd wysyłania" +msgstr "Błąd wysyłania" -#: ImageBrowser.py:884 -msgid "Error computing MD5:" -msgstr "Błąd przy obliczaniu MD5:" - -#: ImageBrowser.py:946 +#: ImageBrowser.py:695 msgid "Błąd" msgstr "Błąd" -#: ImageBrowser.py:946 +#: ImageBrowser.py:695 msgid "Nie można załadować obrazka:" msgstr "Nie można załadować obrazka:" -#: ImageBrowser.py:1156 ImageBrowser.py:1166 +#: ImageBrowser.py:923 ImageBrowser.py:935 #, python-brace-format msgid "Warning: Tag '{tag}' not found in implication graph" msgstr "Ostrzeżenie: Tag '{tag}' nie został znaleziony w grafie implikacji" -#: ImageBrowser.py:1396 +#: ImageBrowser.py:1144 msgid "Tagger przetwarza..." msgstr "Tagger przetwarza..." -#: ImageBrowser.py:1421 -#, python-brace-format -msgid "Wysyłam plik {base_file_name}..." -msgstr "Wysyłam plik {base_file_name}..." - -#: ImageBrowser.py:1462 -msgid "Wysyłanie zakończone powodzeniem!" -msgstr "Wysyłanie zakończone powodzeniem!" - -#: ImageBrowser.py:1466 ImageBrowser.py:1475 -#, python-brace-format -msgid "" -"Wysyłanie zakończone błędem.\n" -"Status: {status_code}\n" -"Treść: {text}" -msgstr "" -"Wysyłanie zakończone błędem.\n" -"Status: {status_code}\n" -"Treść: {text}" - -#: ImageBrowser.py:1481 ImageBrowser.py:1484 -msgid "Wysyłanie" -msgstr "Wysyłanie" - -#: ImageBrowser.py:1497 -msgid "Błąd wysyłania" -msgstr "Błąd wysyłania" - -#: ImageBrowser.py:1517 -msgid "Błąd edycji" -msgstr "Błąd edycji" - -#: ImageBrowser.py:1517 -msgid "Post nie został znaleziony dla tego pliku" -msgstr "Post nie został znaleziony dla tego pliku" - -#: ImageBrowser.py:1527 -#, python-brace-format -msgid "Aktualizuję tagi dla {base_file_name}..." -msgstr "Aktualizuję tagi dla {base_file_name}..." - -#: ImageBrowser.py:1537 ImageBrowser.py:1547 ImageBrowser.py:1578 -msgid "Operacja anulowana" -msgstr "Operacja anulowana" - -#: ImageBrowser.py:1588 -msgid "Tagi zostały zaktualizowane!" -msgstr "Tagi zostały zaktualizowane!" - -#: ImageBrowser.py:1590 -msgid "Sukces edycji" -msgstr "Sukces edycji" - -#: ImageBrowser.py:1596 -#, python-brace-format -msgid "" -"Błąd podczas aktualizacji tagów\n" -"Status: {code}" -msgstr "" -"Błąd podczas aktualizacji tagów\n" -"Status: {code}" - -#: ImageBrowser.py:1600 -msgid "Treść:" -msgstr "Treść:" - -#: ImageBrowser.py:1604 -msgid "Krytyczny błąd edycji" -msgstr "Krytyczny błąd edycji" - -#: ImageBrowser.py:1616 +#: ImageBrowser.py:1161 msgid "Potwierdzenie" msgstr "Potwierdzenie" -#: ImageBrowser.py:1618 +#: ImageBrowser.py:1163 msgid "" "Czy na pewno chcesz wrzucić wszystkie niewrzucone pliki?\n" "Każdy z nich zostanie oznaczony tagiem 'meta:auto_upload'.\n" @@ -387,21 +425,6 @@ msgstr "" "Każdy z nich zostanie oznaczony tagiem 'meta:auto_upload'.\n" "Upewnij się, że tagi są poprawne!" -#: ImageBrowser.py:1642 -msgid "Anulowano operację!" -msgstr "Operacja anulowana!" - -#: ImageBrowser.py:1650 -#, python-brace-format -msgid "" -"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" -msgstr "" -"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" - -#: ImageBrowser.py:1666 -msgid "Przesłano pliki!" -msgstr "Pliki zostały przesłane!" - #: ProcessingDialog.py:15 msgid "Processing..." msgstr "Przetwarzanie..." @@ -435,9 +458,8 @@ msgid "Błąd przy pobieraniu tagów artystów:" msgstr "Błąd przy pobieraniu tagów artystów:" #: tag_processing.py:208 -#, fuzzy msgid "Nie można sparsować parametrów." -msgstr "Nie można załadować obrazka:" +msgstr "Nie można sparsować parametrów." #: tag_processing.py:295 msgid "Błąd podczas odczytu tag_aliases:" diff --git a/pyproject.toml b/pyproject.toml index 0b9deab..9d67889 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "kapitanbooru-uploader" -version = "0.8.3" +version = "0.9.0" description = "A GUI application for uploading images to KapitanBooru" authors = [{ name = "Michał Leśniak", email = "kapitan@mlesniak.pl" }] dependencies = [