Keep e621 metadata on downloaded posts; clear up account roles

Roles:
- JakeBreathild is now staff + superuser (the real account); the 'jake'
  smoke-test account was demoted to a regular user
- /me exposes is_superuser and the account page shows an admin badge

e621 metadata:
- MediaItem gains e621_post_id and e621_data (trimmed post payload:
  tags by category, rating, score, favourites, comments, sources,
  description, pools, relationships, file info, uploader)
- Download to Library accepts the post payload from the SPA and stores
  it; the item's custom rating is seeded from the e621 rating when empty
- Library detail shows an e621 metadata card: link to the in-app post,
  score/favourites/comments, taxonomy-coloured tags, DText description,
  sources and pools; grid cards get an e621 badge and fall back to the
  e621 rating for their colour (display_rating)
- Guest visibility now also considers e621 tags, so downloaded explicit
  content is hidden from anonymous visitors
This commit is contained in:
2026-09-17 10:27:06 -05:00
parent e5cc63b0cc
commit ebac3ac922
13 changed files with 343 additions and 29 deletions
+12 -3
View File
@@ -21,6 +21,7 @@ export function MediaCard({ item }: { item: MediaItem }) {
item.kind === "video"
? `/api/files/${item.j_id}/thumbnail/`
: `/api/files/${item.j_id}/raw/`;
const rating = item.display_rating;
return (
<Link
@@ -34,14 +35,22 @@ export function MediaCard({ item }: { item: MediaItem }) {
loading="lazy"
className="h-full w-full object-cover transition duration-200 group-hover:scale-[1.03]"
/>
{item.rating ? (
{rating ? (
<span
className={cn(
"absolute left-2 top-2 rounded-full border px-2 py-0.5 font-mono text-[11px] font-medium",
ratingStyles[item.rating],
ratingStyles[rating],
)}
>
{ratingLabels[item.rating]}
{ratingLabels[rating]}
</span>
) : null}
{item.e621_post_id ? (
<span
className="absolute left-2 top-8 rounded-sm bg-ctp-lavender/90 px-1.5 py-0.5 font-mono text-[10px] font-medium text-ctp-crust"
title={`e621 post #${item.e621_post_id}`}
>
e621
</span>
) : null}
<span className="absolute right-2 top-2 rounded-sm bg-ctp-crust/80 px-1.5 py-0.5 font-mono text-[10px] uppercase text-ctp-overlay1">
+12 -10
View File
@@ -33,17 +33,19 @@ export function TagChip({
onClick?: () => void;
}) {
const style = TAG_STYLES[category ?? "unknown"] ?? TAG_STYLES.unknown;
const className = cn(
"rounded border px-1.5 py-0.5 font-mono text-[11px] transition",
style,
);
const label = count !== undefined ? `${tag} (${count})` : tag;
if (!onClick) {
return <span className={className}>{label}</span>;
}
return (
<button
type="button"
onClick={onClick}
className={cn(
"rounded border px-1.5 py-0.5 font-mono text-[11px] transition",
style,
)}
>
{tag}
{count !== undefined ? ` (${count})` : ""}
<button type="button" onClick={onClick} className={className}>
{label}
</button>
);
}