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.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(