Files
mcma-backend/app/infrastructure/sources/registry.py
T
Senko-san 48e3418c7f
Docker Build & Publish / build (push) Has been cancelled
Docker Build & Publish / push (push) Has been cancelled
Docker Build & Publish / Prune old image versions (push) Has been cancelled
feat(sources): local_folder source backend + import pipeline
First ingest path beyond manual upload (plan §1C). Source abstraction +
the first concrete backend, so a homelab can index an existing library.

- domain: SourceBackend/IndexableSource ports + SourceInfo/SourceFile shapes
- infrastructure/sources: LocalFolderSource (walks a mounted dir, idempotent
  source_id = relative path) + registry built from settings
- application: LibraryImportService — batch sibling of UploadService; dedup on
  (source, source_id), copy into storage, minimal track (metadata_status=pending,
  enrichment fills the rest in 1D), per-file failures isolated
- workers: scan_local_folder arq task (registered) + enqueue helper (503 if
  Redis down)
- api: GET /sources, POST /sources/{source}/scan (admin, enqueues), /health
- config: LOCAL_MEDIA_IMPORT_PATH; README + .env.example documented
- tests: scanner, registry, import service (fakes) + DB-gated sources API path

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:02:09 +03:00

42 lines
1.6 KiB
Python

"""Source registry — selection + enumeration of configured backends.
Built from settings at the composition root. Only sources that are configured
are registered (e.g. ``local`` appears only when ``LOCAL_MEDIA_IMPORT_PATH`` is
set), so enumeration reflects what the instance can actually use.
"""
from typing import cast
from app.core.config import Settings
from app.domain.errors import NotFoundError, ValidationError
from app.domain.ports import IndexableSource, SourceBackend
from app.domain.sources import SourceInfo
from app.infrastructure.sources.local_folder import LocalFolderSource
class SourceRegistry:
def __init__(self, backends: list[SourceBackend]) -> None:
self._by_name = {backend.name: backend for backend in backends}
def get(self, name: str) -> SourceBackend:
backend = self._by_name.get(name)
if backend is None:
raise NotFoundError(f"Source {name!r} is not configured.")
return backend
def indexable(self, name: str) -> IndexableSource:
backend = self.get(name)
if not hasattr(backend, "scan"):
raise ValidationError(f"Source {name!r} cannot be indexed.")
return cast(IndexableSource, backend)
def infos(self) -> list[SourceInfo]:
return [backend.info() for backend in self._by_name.values()]
def build_source_registry(settings: Settings) -> SourceRegistry:
backends: list[SourceBackend] = []
if settings.local_media_import_path is not None:
backends.append(LocalFolderSource(settings.local_media_import_path))
return SourceRegistry(backends)