Files
JakeBreath 03dd235f3a Follows: followed tags/pools, feeds, unseen badges and blacklist cloud
Backend (new apps.follows):
- FollowedTag/FollowedPool/FollowedPost models; per-user follows with
  unseen tracking, plus FollowCloud for the cached blacklist cloud.
- Two periodic commands sharing one fetch path: sync_followed_tags and
  sync_followed_pools fetch each followed tag/pool's newest posts (one
  e621 search per unique follow), store unseen feed rows, refresh covers
  and pool metadata; both fall back to anonymous e621 access.
- API: /api/follows/tags|pools (follow, unfollow, mark seen), a merged
  feed with per-follow filtering, and /api/follows/cloud/ which rebuilds
  the blacklisted-tag cloud in a daemon thread when its 10 min cache is
  stale (polling returns building/ready).
- e621 client now supports anonymous reads; trimmed posts carry preview
  URLs for covers and feed tiles.

Frontend:
- /followed page: follow forms, cover cards with unseen badges and
  Mark seen, merged feed with filter/unseen toggle, and a blacklist
  cloud panel that polls while building. Followed nav entry added.
2026-09-17 14:09:30 -05:00

63 lines
2.1 KiB
Python

from django.core.management.base import BaseCommand, CommandError
from apps.follows import sync
from apps.follows.models import FollowedPool
from apps.library import e621
class Command(BaseCommand):
help = (
"Fetch new posts for every followed pool: fills each feed, refreshes "
"the card cover (latest image), pool name/count and unseen badges."
)
def add_arguments(self, parser):
parser.add_argument(
"--user",
help="Only sync follows belonging to this username "
"(default: every user's follows).",
)
parser.add_argument(
"--limit",
type=int,
default=sync.FEED_LIMIT,
help=f"Posts fetched per pool (default {sync.FEED_LIMIT}).",
)
def handle(self, *args, **options):
username = options.get("user")
if username and sync.preferred_fetch_user(username) is None:
raise CommandError(f"No such user: {username}")
fetch_user = sync.preferred_fetch_user(username)
follows = FollowedPool.objects.select_related("user").order_by(
"pool_id", "user_id"
)
if username:
follows = follows.filter(user__username=username)
follows = list(follows)
grouped = {}
for follow in follows:
grouped.setdefault(follow.pool_id, []).append(follow)
created = 0
for pool_id, group in grouped.items():
try:
posts = sync.search_posts(
fetch_user, f"pool:{pool_id}", options["limit"]
)
except e621.E621Error as exc:
raise CommandError(f"Aborted at pool #{pool_id}: {exc}")
for follow in group:
created += sync.sync_pool_follow(
follow, fetch_user, posts=posts
)
identity = getattr(fetch_user, "username", None) or "anonymous"
self.stdout.write(
self.style.SUCCESS(
f"Synced {len(grouped)} pool(s) across {len(follows)} follow(s) "
f"as {identity}: {created} new post(s)."
)
)