mirror of
https://github.com/wizarrrr/wizarr.git
synced 2026-07-30 23:07:19 -04:00
This fixes a long-standing issue where scanning a library would cause an unintended cascade delete of the invite_library table and would reset the associated invite's permissions.
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
"""Library scanning service for media servers.
|
|
|
|
This service handles scanning and synchronizing library metadata from media servers
|
|
into the local database during application startup.
|
|
|
|
This implementation performs an upsert (update existing, insert new) and soft-deletes
|
|
(or removes) libraries that no longer exist on the remote server. Libraries that are
|
|
referenced by invitations are preserved (disabled) instead of being hard-deleted so
|
|
invite<->library associations remain intact.
|
|
"""
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Timeout for individual server library scans (in seconds)
|
|
# This prevents a single unreachable server from blocking startup
|
|
LIBRARY_SCAN_TIMEOUT = 15
|
|
|
|
|
|
def scan_all_server_libraries(show_logs: bool = True) -> tuple[int, list[str]]:
|
|
"""Scan libraries for all configured media servers.
|
|
|
|
Args:
|
|
show_logs: Whether to output log messages during scanning
|
|
|
|
Returns:
|
|
Tuple of (total_scanned, error_messages)
|
|
- total_scanned: Number of libraries successfully scanned
|
|
- error_messages: List of error messages for failed scans
|
|
"""
|
|
from sqlalchemy import inspect
|
|
|
|
from app.extensions import db
|
|
from app.models import Library, MediaServer, invite_libraries
|
|
from app.services.media.service import get_client_for_media_server
|
|
|
|
# Check if the library table exists (in case migrations haven't run yet)
|
|
inspector = inspect(db.engine)
|
|
if not inspector.has_table("library"):
|
|
if show_logs:
|
|
logger.info("Library table doesn't exist yet - skipping scan")
|
|
raise Exception("Library table not found - run migrations first")
|
|
|
|
servers = MediaServer.query.all()
|
|
total_scanned = 0
|
|
errors = []
|
|
|
|
for server in servers:
|
|
try:
|
|
client = get_client_for_media_server(server)
|
|
libraries_dict = client.libraries() # {external_id: name}
|
|
|
|
# Normalize to dict if client returned list-like
|
|
if not isinstance(libraries_dict, dict):
|
|
libraries_dict = {
|
|
str(k): str(v)
|
|
for k, v in (
|
|
libraries_dict.items()
|
|
if hasattr(libraries_dict, "items")
|
|
else libraries_dict
|
|
)
|
|
}
|
|
|
|
# Load existing libraries for this server keyed by external_id
|
|
existing_libs = {
|
|
lib.external_id: lib
|
|
for lib in Library.query.filter_by(server_id=server.id).all()
|
|
}
|
|
old_count = len(existing_libs)
|
|
|
|
incoming_ids = set()
|
|
updated_count = 0
|
|
inserted_count = 0
|
|
disabled_count = 0
|
|
deleted_count = 0
|
|
|
|
# Upsert incoming libraries: update existing, insert new
|
|
for external_id, name in libraries_dict.items():
|
|
incoming_ids.add(str(external_id))
|
|
if external_id in existing_libs:
|
|
lib = existing_libs[external_id]
|
|
# Update mutable attributes while preserving primary key
|
|
lib.name = name
|
|
lib.enabled = True
|
|
updated_count += 1
|
|
else:
|
|
lib = Library(
|
|
external_id=external_id,
|
|
name=name,
|
|
server_id=server.id,
|
|
enabled=True,
|
|
)
|
|
db.session.add(lib)
|
|
inserted_count += 1
|
|
total_scanned += 1
|
|
|
|
# Handle libraries that used to exist but weren't returned by the server
|
|
for ext, lib in existing_libs.items():
|
|
if ext not in incoming_ids:
|
|
# If this library is referenced by any invitation, disable it to preserve associations
|
|
referenced = db.session.execute(
|
|
invite_libraries.select().where(
|
|
invite_libraries.c.library_id == lib.id
|
|
)
|
|
).first()
|
|
if referenced:
|
|
if lib.enabled:
|
|
lib.enabled = False
|
|
disabled_count += 1
|
|
else:
|
|
# Safe to remove since no invites reference it
|
|
db.session.delete(lib)
|
|
deleted_count += 1
|
|
|
|
db.session.commit()
|
|
|
|
if show_logs:
|
|
logger.info(
|
|
f"Refreshed {len(libraries_dict)} libraries for {server.name} "
|
|
f"(existing={old_count}, updated={updated_count}, inserted={inserted_count}, "
|
|
f"disabled={disabled_count}, deleted={deleted_count})"
|
|
)
|
|
except Exception as server_exc:
|
|
# Rollback on error to keep session clean
|
|
db.session.rollback()
|
|
error_msg = f"Failed to scan libraries for {server.name}: {server_exc}"
|
|
errors.append(error_msg)
|
|
if show_logs:
|
|
logger.warning(error_msg)
|
|
|
|
return total_scanned, errors
|