""" Scanner service for initial library scan and per-user source root bootstrap. """ import os import logging from sqlalchemy import select from app.database import AsyncSessionLocal from app.models import SourceRoot from app.models.user import User from app.tasks.scan import scan_all_source_roots from app.config import settings logger = logging.getLogger(__name__) async def bootstrap_user_source_root(user: User, session=None) -> None: """Create the media directory and a source root for a user. Called when a new user is created (by the admin or the setup endpoint). If the user already has a source root, this is a no-op. """ own_session = session is None if own_session: session = AsyncSessionLocal() try: # Check if user already has a source root result = await session.execute( select(SourceRoot).where(SourceRoot.user_id == user.id) ) if result.scalar_one_or_none() is not None: return os.makedirs(user.media_path, exist_ok=True) source_root = SourceRoot( name=f"{user.username}'s Library", path=user.media_path, user_id=user.id, ) session.add(source_root) if own_session: await session.commit() else: await session.flush() logger.info( f"Bootstrapped source root for user '{user.username}': " f"{user.media_path}" ) finally: if own_session: await session.close() async def bootstrap_default_source_root() -> None: """Legacy bootstrap — for existing installs that have source roots without user_id (pre-auth migration). On fresh installs, source roots are created per-user via bootstrap_user_source_root. If there are already source roots in the DB, this is a no-op. """ async with AsyncSessionLocal() as session: result = await session.execute(select(SourceRoot)) if result.scalars().first() is not None: return # Already have source roots. # No source roots and no users means fresh install — the setup # endpoint will create the first user + source root. user_count = (await session.execute( select(User) )).scalars().first() if user_count is None: logger.info( "No users or source roots — waiting for first-run setup." ) return async def start_initial_scan(): """Start the initial library scan and optionally the file watcher. The file watcher uses a Redis lock to ensure only one instance runs across all workers, so it's safe to dispatch on every startup — only the first one will actually watch, the rest exit immediately. """ try: scan_all_source_roots.delay() logger.info("Initial scan queued successfully") except Exception as e: logger.error(f"Failed to start initial scan: {e}") # Start the file watcher if enabled in config. from app.config import settings if settings.scanner.watch: try: from app.tasks.scan import watch_folders # Countdown gives the initial scan time to register source roots # before the watcher tries to load them. watch_folders.apply_async(countdown=10) logger.info("File watcher queued (Redis-locked, single instance)") except Exception as e: logger.warning(f"Could not queue file watcher: {e}")