fc3d: SourceRecord surfaces consecutive_failures + server-derived next_check_at

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 16:03:33 -04:00
parent 2e8aa8c3ce
commit cd6983f728
+27 -18
View File
@@ -8,8 +8,9 @@ from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from ..models import Artist, Source from ..models import Artist, ImportSettings, Source
from .platforms import known_platform_keys 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 # Re-exported for back-compat with FC-3a; the registry in
# services/platforms.py is the single source of truth. # services/platforms.py is the single source of truth.
@@ -59,6 +60,8 @@ class SourceRecord:
last_checked_at: str | None last_checked_at: str | None
last_error: str | None last_error: str | None
check_interval_override: int | None check_interval_override: int | None
consecutive_failures: int
next_check_at: str | None
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
@@ -73,6 +76,8 @@ class SourceRecord:
"last_checked_at": self.last_checked_at, "last_checked_at": self.last_checked_at,
"last_error": self.last_error, "last_error": self.last_error,
"check_interval_override": self.check_interval_override, "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") raise InvalidConfigError("config_overrides must be a JSON object")
return config return config
async def _row_to_record(self, source: Source) -> SourceRecord: async def _load_settings(self) -> ImportSettings:
artist = (await self.session.execute( return (await self.session.execute(
select(Artist.name, Artist.slug).where(Artist.id == source.artist_id) select(ImportSettings).where(ImportSettings.id == 1)
)).one() )).scalar_one()
def _build_record(
self, source: Source, artist: Artist, settings: ImportSettings,
) -> SourceRecord:
nxt = compute_next_check_at(source, artist, settings)
return SourceRecord( return SourceRecord(
id=source.id, id=source.id,
artist_id=source.artist_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_checked_at=source.last_checked_at.isoformat() if source.last_checked_at else None,
last_error=source.last_error, last_error=source.last_error,
check_interval_override=source.check_interval_override, 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]: async def list(self, artist_id: int | None = None) -> list[SourceRecord]:
stmt = ( stmt = (
select(Source, Artist.name, Artist.slug) select(Source, Artist)
.join(Artist, Artist.id == Source.artist_id) .join(Artist, Artist.id == Source.artist_id)
.order_by(Artist.name.asc(), Source.id.asc()) .order_by(Artist.name.asc(), Source.id.asc())
) )
if artist_id is not None: if artist_id is not None:
stmt = stmt.where(Source.artist_id == artist_id) stmt = stmt.where(Source.artist_id == artist_id)
rows = (await self.session.execute(stmt)).all() rows = (await self.session.execute(stmt)).all()
return [ settings = await self._load_settings()
SourceRecord( return [self._build_record(s, a, settings) for s, a in rows]
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
]
async def get(self, source_id: int) -> SourceRecord | None: async def get(self, source_id: int) -> SourceRecord | None:
source = (await self.session.execute( source = (await self.session.execute(