diff --git a/backend/app/services/source_service.py b/backend/app/services/source_service.py index f5ff108..df75e3e 100644 --- a/backend/app/services/source_service.py +++ b/backend/app/services/source_service.py @@ -8,8 +8,9 @@ from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from ..models import Artist, Source +from ..models import Artist, ImportSettings, Source from .platforms import known_platform_keys +from .scheduler_service import compute_next_check_at # Re-exported for back-compat with FC-3a; the registry in # services/platforms.py is the single source of truth. @@ -59,6 +60,8 @@ class SourceRecord: last_checked_at: str | None last_error: str | None check_interval_override: int | None + consecutive_failures: int + next_check_at: str | None def to_dict(self) -> dict: return { @@ -73,6 +76,8 @@ class SourceRecord: "last_checked_at": self.last_checked_at, "last_error": self.last_error, "check_interval_override": self.check_interval_override, + "consecutive_failures": self.consecutive_failures, + "next_check_at": self.next_check_at, } @@ -114,10 +119,15 @@ class SourceService: raise InvalidConfigError("config_overrides must be a JSON object") return config - async def _row_to_record(self, source: Source) -> SourceRecord: - artist = (await self.session.execute( - select(Artist.name, Artist.slug).where(Artist.id == source.artist_id) - )).one() + async def _load_settings(self) -> ImportSettings: + return (await self.session.execute( + select(ImportSettings).where(ImportSettings.id == 1) + )).scalar_one() + + def _build_record( + self, source: Source, artist: Artist, settings: ImportSettings, + ) -> SourceRecord: + nxt = compute_next_check_at(source, artist, settings) return SourceRecord( id=source.id, artist_id=source.artist_id, @@ -130,29 +140,28 @@ class SourceService: last_checked_at=source.last_checked_at.isoformat() if source.last_checked_at else None, last_error=source.last_error, check_interval_override=source.check_interval_override, + consecutive_failures=source.consecutive_failures or 0, + next_check_at=nxt.isoformat() if nxt else None, ) + async def _row_to_record(self, source: Source) -> SourceRecord: + artist = (await self.session.execute( + select(Artist).where(Artist.id == source.artist_id) + )).scalar_one() + settings = await self._load_settings() + return self._build_record(source, artist, settings) + async def list(self, artist_id: int | None = None) -> list[SourceRecord]: stmt = ( - select(Source, Artist.name, Artist.slug) + select(Source, Artist) .join(Artist, Artist.id == Source.artist_id) .order_by(Artist.name.asc(), Source.id.asc()) ) if artist_id is not None: stmt = stmt.where(Source.artist_id == artist_id) rows = (await self.session.execute(stmt)).all() - return [ - SourceRecord( - id=s.id, artist_id=s.artist_id, - artist_name=name, artist_slug=slug, - platform=s.platform, url=s.url, enabled=s.enabled, - config_overrides=s.config_overrides, - last_checked_at=s.last_checked_at.isoformat() if s.last_checked_at else None, - last_error=s.last_error, - check_interval_override=s.check_interval_override, - ) - for s, name, slug in rows - ] + settings = await self._load_settings() + return [self._build_record(s, a, settings) for s, a in rows] async def get(self, source_id: int) -> SourceRecord | None: source = (await self.session.execute(