fix(sync): refresh a channel's details once they are a week old

Channel metadata (banner, avatar, description, counts) was fetched only the first time a
channel was seen: the subscription import asked only for channels with no details, and the
channel page only for ones missing a join date. 394 of 397 subscribed channels had July
metadata. Both now also refresh details older than DETAILS_MAX_AGE (7 days), through one
shared staleness rule; channels.list costs 1 unit per 50 channels.
This commit is contained in:
2026-09-26 21:45:46 +02:00
parent 58878958d8
commit 663f30feeb
3 changed files with 140 additions and 9 deletions
+26 -5
View File
@@ -1,8 +1,8 @@
"""Import the authenticated user's YouTube subscriptions and channel metadata."""
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from app import logs
@@ -47,6 +47,27 @@ def _external_links(branding: dict) -> list | None:
return out or None
# How old a channel's metadata may get before it is fetched again. Banners, avatars, descriptions and
# counts change on YouTube, and an old banner URL stops resolving at all — without a refresh a channel
# kept whatever it had the day it was first seen. A week keeps counts roughly current at almost no
# cost: `channels.list` is 1 quota unit per 50 channels, and only the stale ones are asked for.
DETAILS_MAX_AGE = timedelta(days=7)
def details_stale(channel: Channel, now: datetime) -> bool:
"""Whether `channel`'s metadata is missing or older than DETAILS_MAX_AGE (see `details_stale_clause`)."""
synced = channel.details_synced_at
return synced is None or synced < now - DETAILS_MAX_AGE
def details_stale_clause(now: datetime):
"""`details_stale` as a WHERE clause, so the batch import and a single channel agree on "stale"."""
return or_(
Channel.details_synced_at.is_(None),
Channel.details_synced_at < now - DETAILS_MAX_AGE,
)
def apply_channel_details(db: Session, items: list[dict]) -> None:
now = datetime.now(timezone.utc)
for item in items:
@@ -92,7 +113,7 @@ def should_prune(fetched_count: int, existing_count: int) -> bool:
def import_subscriptions(db: Session, user: User) -> dict:
"""Pull all subscriptions for `user`, upsert channels + subscription links, prune
channels the user has unsubscribed from on YouTube, and fetch channel details for
any channel we don't have details for yet."""
any channel whose details are missing or stale (`DETAILS_MAX_AGE`)."""
fetched: dict[str, dict] = {}
new_channels = 0
@@ -145,12 +166,12 @@ def import_subscriptions(db: Session, user: User) -> dict:
removed += 1
db.commit()
# Fetch details for channels that don't have them yet.
# Fetch details for channels that don't have them yet, or had them too long ago.
need_details = (
db.execute(
select(Channel).where(
Channel.id.in_(fetched.keys()),
Channel.details_synced_at.is_(None),
details_stale_clause(datetime.now(timezone.utc)),
)
)
.scalars()