Files
SemesterapparatsManager/src/ui/widgets/signature_update.py

272 lines
10 KiB
Python

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")