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.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
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)."
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from apps.follows import sync
|
||||
from apps.follows.models import FollowedTag
|
||||
from apps.library import e621
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Fetch new posts for every followed tag: fills each feed, refreshes "
|
||||
"the card cover (latest image) 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 tag (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 = FollowedTag.objects.select_related("user").order_by(
|
||||
"tag", "user_id"
|
||||
)
|
||||
if username:
|
||||
follows = follows.filter(user__username=username)
|
||||
follows = list(follows)
|
||||
grouped = {}
|
||||
for follow in follows:
|
||||
grouped.setdefault(follow.tag, []).append(follow)
|
||||
|
||||
created = 0
|
||||
for tag, group in grouped.items():
|
||||
try:
|
||||
posts = sync.search_posts(fetch_user, tag, options["limit"])
|
||||
except e621.E621Error as exc:
|
||||
raise CommandError(f"Aborted at tag '{tag}': {exc}")
|
||||
for follow in group:
|
||||
created += sync.sync_tag_follow(
|
||||
follow, fetch_user, posts=posts
|
||||
)
|
||||
|
||||
identity = getattr(fetch_user, "username", None) or "anonymous"
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Synced {len(grouped)} tag(s) across {len(follows)} follow(s) "
|
||||
f"as {identity}: {created} new post(s)."
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user