import os import time from concurrent.futures import ThreadPoolExecutor from queue import Empty, Queue from PySide6 import QtCore, QtWidgets from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from src.backend.catalogue import Catalogue from src.backend.database import Database from src.backend.webadis import get_book_medianr from src.logic.SRU import SWB from src.shared.logging import log from .widget_sources.admin_update_signatures_ui import Ui_Dialog class MedianrThread(QtCore.QThread): progress = QtCore.Signal(int) currtot = QtCore.Signal(int, int) def __init__(self, books=None, thread_count=6): super().__init__() self.books = books or [] self.total = 0 # Database instances are not always thread-safe across threads; create one per worker. # Keep a shared auth that is safe to read. db_main = Database() self.auth = db_main.getWebADISAuth() if self.auth == ("", ""): raise ValueError("WebADIS authentication not set in database.") self.thread_count = max(1, thread_count) self._stop_requested = False def run(self): # Use ThreadPoolExecutor to parallelize I/O-bound tasks. total_books = len(self.books) if total_books == 0: log.debug("MedianrThread: no books to process") return chunk_size = (total_books + self.thread_count - 1) // self.thread_count chunks = [ self.books[i : i + chunk_size] for i in range(0, total_books, chunk_size) ] # queue for worker results and progress q = Queue() def medianrworker(chunk: list[dict]): # Each worker creates its own Database instance for thread-safety db = Database() for book in chunk: if self._stop_requested: break try: booknr = get_book_medianr( book["bookdata"].signature, db.getApparatNrByBookId(book["id"]), self.auth, ) log.debug( f"Book ID {book['id']} - Signature {book['bookdata'].signature} - Medianr {booknr}" ) book_data = book["bookdata"] book_data.medianr = booknr db.updateBookdata(book["id"], book_data) q.put(("progress", 1)) except Exception as e: log.error(f"Medianr worker error for book {book}: {e}") q.put(("progress", 1)) time.sleep(10) q.put(("done", None)) processed = 0 finished_workers = 0 with ThreadPoolExecutor(max_workers=len(chunks)) as ex: futures = [ex.submit(medianrworker, ch) for ch in chunks] log.info( f"Launched {len(futures)} worker thread(s) for {total_books} entries: {[len(ch) for ch in chunks]} entries per thread." ) # aggregate progress while finished_workers < len(chunks): try: kind, payload = q.get(timeout=0.1) except Empty: continue if kind == "progress": processed += int(payload) self.progress.emit(processed) # emit currtot with processed and current chunk total as best-effort self.currtot.emit(processed, total_books) elif kind == "done": finished_workers += 1 # ensure final progress reached total_books self.progress.emit(total_books) self.currtot.emit(total_books, total_books) def stop(self): """Request the thread to stop early.""" self._stop_requested = True def process_chunk(self, books_chunk): # kept for backward compatibility but not used by run(); still usable externally db = Database() for index, book in enumerate(books_chunk): try: booknr = get_book_medianr( book["bookdata"].signature, db.getApparatNrByBookId(book["id"]), self.auth, ) log.debug( f"Book ID {book['id']} - Signature {book['bookdata'].signature} - Medianr {booknr}" ) book_data = book["bookdata"] book_data.medianr = booknr db.updateBookdata(book["id"], book_data) except Exception as e: log.error(f"Medianr process_chunk error for book {book}: {e}") self.progress.emit(index + 1) self.currtot.emit(self.total + 1, len(books_chunk)) self.total += 1 class UpdaterThread(QtCore.QThread): progress = QtCore.Signal(int) currtot = QtCore.Signal(int, int) def __init__(self, books=None): super().__init__() self.books = books self.db = Database() self.catalogue = Catalogue() def run(self): total_books = len(self.books) for index, book in enumerate(self.books): try: id = book["id"] bookdata = book["bookdata"] ppn = bookdata.ppn result = self.catalogue.get_book(ppn) if result: log.debug(f"Updating book {id} with ppn {ppn}") bookdata.signature = result.signature # #print(bookdata) self.db.updateBookdata(id, bookdata) else: log.debug(f"No result for {ppn}") # #print(f"No result for {ppn}") # self.db.deleteBook(id) except Exception as e: log.error(f"Error updating book {book}: {e}") self.progress.emit(index + 1) self.currtot.emit(index + 1, total_books) class CompleterThread(QtCore.QThread): progress = QtCore.Signal(int) currtot = QtCore.Signal(int, int) def __init__(self, books=None): super().__init__() self.books = books self.db = Database() self.catalogue = Catalogue() self.swb = SWB() def run(self): total_books = len(self.books) for index, book in enumerate(self.books): try: id = book["id"] bookdata = book["bookdata"] ppn = bookdata.ppn cat_book = self.catalogue.get_book(f"kid:{ppn}") swb_version = self.swb.getBooks(["pica.bib=20735", f"pica.ppn={ppn}"])[ 0 ] if cat_book: merged = cat_book.merge(swb_version) else: merged = swb_version # compare original_book with merged, if different, update db if bookdata != merged: # #print(f"Updating book {id} with ppn {ppn}") # #print("Original book:", bookdata) # #print("Merged book:", merged) self.db.updateBookdata(id, merged) except Exception as e: log.error(f"Error updating book {book}: {e}") # else: # #print(f"No result for {ppn}") # self.db.deleteBook(id) self.progress.emit(index + 1) self.currtot.emit(index + 1, total_books) class UpdateSignatures(QtWidgets.QDialog, Ui_Dialog): def __init__(self, parent=None): super(UpdateSignatures, self).__init__(parent) self.setupUi(self) self.setWindowTitle("Updating signatures...") self.progressBar.setValue(0) self.btn_update_signatures.clicked.connect(self.update_signatures) self.btn_add_missing_data.clicked.connect(self.add_missing) self.db = Database() self.catalogue = Catalogue() self.player = QMediaPlayer() self.audio_output = QAudioOutput() self.spin_thread_count.setMaximum(os.cpu_count()) self.btn_add_medianr.clicked.connect(self.add_medianr) def play_sound(self, sound_file: str): self.player.setAudioOutput(self.audio_output) self.audio_output.setVolume(50) self.player.setSource(QtCore.QUrl.fromLocalFile(f"src/sounds/{sound_file}")) self.player.play() def update_signatures(self): books = self.db.getAllBooks() total_books = len(books) self.progressBar.setMaximum(total_books) self.updater = UpdaterThread(books) self.updater.progress.connect(self.update_progress) self.updater.finished.connect(self.updater.deleteLater) self.updater.start() def add_medianr(self): books = self.db.getAllBooks() books = [book for book in books if book["bookdata"].medianr is None] total_books = len(books) self.progressBar.setMaximum(total_books) self.medianr_thread = MedianrThread(books, self.spin_thread_count.value()) self.medianr_thread.progress.connect(self.update_progress) self.medianr_thread.finished.connect(self.medianr_thread.deleteLater) self.medianr_thread.start() def add_missing(self): books = self.db.getAllBooks() total_books = len(books) self.progressBar.setMaximum(total_books) incomplete_books = [ book for book in books if any( value in (None, "", "None") for value in book["bookdata"].__dict__.values() ) ] self.completionist = CompleterThread(incomplete_books) self.completionist.progress.connect(self.update_progress) self.completionist.finished.connect(self.completionist.deleteLater) self.completionist.start() def update_progress(self, value): self.progressBar.setValue(value) if value <= self.progressBar.maximum(): self.btn_update_signatures.setEnabled(False) if value == self.progressBar.maximum(): self.btn_update_signatures.setEnabled(True) self.play_sound("ding.mp3")