append pfps
This commit is contained in:
@@ -1,5 +1,20 @@
|
|||||||
# Adrian
|
# Adrian
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**Adrian — 1**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Adrian — 2**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handle: `Axe7Adrian`
|
- Observed handle: `Axe7Adrian`
|
||||||
- Activity: 1392 messages across 29 streams
|
- Activity: 1392 messages across 29 streams
|
||||||
- First seen: 2023-12-29 in `DRACONIANDISCOURSE_2_TIKTOK_SKINWALKERS`
|
- First seen: 2023-12-29 in `DRACONIANDISCOURSE_2_TIKTOK_SKINWALKERS`
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# Agate
|
# Agate
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**Agate**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handle: `LoonyAgate`
|
- Observed handle: `LoonyAgate`
|
||||||
- Activity: 69 messages across 1 stream
|
- Activity: 69 messages across 1 stream
|
||||||
- First seen: 2026-03-27 in `creeper.ts`
|
- First seen: 2026-03-27 in `creeper.ts`
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
# Azure
|
# Azure
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**`guidingflyer530` — 1**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**`guidingflyer530` — 2**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**`actuallynotazure`**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handles: `guidingflyer530`, `actuallynotazure`
|
- Observed handles: `guidingflyer530`, `actuallynotazure`
|
||||||
- Activity: 9826 messages across 40 streams
|
- Activity: 9826 messages across 40 streams
|
||||||
- First seen: 2025-10-29 in `obliterate.ts`
|
- First seen: 2025-10-29 in `obliterate.ts`
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# Beanie
|
# Beanie
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**Beanie**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handle: `beaniee__`
|
- Observed handle: `beaniee__`
|
||||||
- Activity: 1852 messages across 15 streams
|
- Activity: 1852 messages across 15 streams
|
||||||
- First seen: 2026-01-16 in `david.ts`
|
- First seen: 2026-01-16 in `david.ts`
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# Heart and Mind
|
# Heart and Mind
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**Heart (`heart_cccc`)**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Mind (`brush_colourful`)**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handles: `Heart_CCCC` as Heart, `brush_colourful` as Mind
|
- Observed handles: `Heart_CCCC` as Heart, `brush_colourful` as Mind
|
||||||
- Activity: one direct bit in `errands.ts`, then later referenced by chat as a memorable event
|
- Activity: one direct bit in `errands.ts`, then later referenced by chat as a memorable event
|
||||||
- First seen: 2026-01-30 in `errands.ts`
|
- First seen: 2026-01-30 in `errands.ts`
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# Jenni
|
# Jenni
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**Jenni**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handle: `JenniMilano`
|
- Observed handle: `JenniMilano`
|
||||||
- Activity: 38 messages across 1 stream
|
- Activity: 38 messages across 1 stream
|
||||||
- First seen: 2025-10-29 in `obliterate.ts`
|
- First seen: 2025-10-29 in `obliterate.ts`
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
# Noname
|
# Noname
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**Noname — 1**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Noname — 2**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Noname — 3**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Noname — 4**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Noname — 5**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Noname — 6**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handle: `noname106668`
|
- Observed handle: `noname106668`
|
||||||
- Activity: 282 messages across 16 streams
|
- Activity: 282 messages across 16 streams
|
||||||
- First seen: 2023-12-29 in `DRACONIANDISCOURSE_2_TIKTOK_SKINWALKERS`
|
- First seen: 2023-12-29 in `DRACONIANDISCOURSE_2_TIKTOK_SKINWALKERS`
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# NotoriousRooster
|
# NotoriousRooster
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**NotoriousRooster**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handle: `notorious_rooster`
|
- Observed handle: `notorious_rooster`
|
||||||
- Activity: 1015 messages across 50 streams
|
- Activity: 1015 messages across 50 streams
|
||||||
- First seen: 2023-11-02 in `whistleblewor.ts`
|
- First seen: 2023-11-02 in `whistleblewor.ts`
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# RaincloudTheDragon
|
# RaincloudTheDragon
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**RaincloudTheDragon**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handle: `RaincloudTheDragon`
|
- Observed handle: `RaincloudTheDragon`
|
||||||
- Activity in chat exports as a chatter: 97 messages across 32 streams
|
- Activity in chat exports as a chatter: 97 messages across 32 streams
|
||||||
- Canonical role: sovereign host, resurrected digital dragon consciousness
|
- Canonical role: sovereign host, resurrected digital dragon consciousness
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# Starboy
|
# Starboy
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**Starboy**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Observed handle: `Starboy_Journeys`
|
- Observed handle: `Starboy_Journeys`
|
||||||
- Activity: 476 messages across 9 streams
|
- Activity: 476 messages across 9 streams
|
||||||
- First seen: 2025-11-14 in `mcrib.ts`
|
- First seen: 2025-11-14 in `mcrib.ts`
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
# Ubear / Malgru
|
# Ubear / Malgru
|
||||||
|
|
||||||
|
<!-- pfp:chat-exports -->
|
||||||
|
|
||||||
|
## Profile pictures
|
||||||
|
|
||||||
|
**`verify52w`**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**`sky_city_2013`**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**`ancientmalgru`**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- /pfp:chat-exports -->
|
||||||
|
|
||||||
|
|
||||||
- Canonical name: `Ubear`
|
- Canonical name: `Ubear`
|
||||||
- Observed handles: `verify52w`, `Sky_City_2013`, `AncientMalgru`, `AnCIentmalGru`
|
- Observed handles: `verify52w`, `Sky_City_2013`, `AncientMalgru`, `AnCIentmalGru`
|
||||||
- Possible additional handle: `imnoob87`
|
- Possible additional handle: `imnoob87`
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,301 @@
|
|||||||
|
"""
|
||||||
|
Scan TwitchDownloader chat JSONs for roster logins, download distinct profile images
|
||||||
|
(commenter.logo) as JPEGs under Story/pfp/, and print markdown snippets (or patch files).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python tools/pfp_from_chat.py [--chat-root PATH] [--story-root PATH] [--no-write-md]
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
chat-root: MIXER_TWITCH_CHAT env, else Windows Synology path used in this project.
|
||||||
|
Writes JPEGs under Story/pfp/<slug>/ and inserts a marked section into each roster .md.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
# slug -> list of twitch logins (lowercase)
|
||||||
|
ROSTER: dict[str, list[str]] = {
|
||||||
|
"adrian": ["axe7adrian"],
|
||||||
|
"noname": ["noname106668"],
|
||||||
|
"agate": ["loonyagate"],
|
||||||
|
"notorious-rooster": ["notorious_rooster"],
|
||||||
|
"ubear": ["verify52w", "sky_city_2013", "ancientmalgru"],
|
||||||
|
"azure": ["guidingflyer530", "actuallynotazure"],
|
||||||
|
"starboy": ["starboy_journeys"],
|
||||||
|
"jenni": ["jennimilano"],
|
||||||
|
"heart-and-mind": ["heart_cccc", "brush_colourful"],
|
||||||
|
"beanie": ["beaniee__"],
|
||||||
|
"raincloud": ["raincloudthedragon"],
|
||||||
|
}
|
||||||
|
|
||||||
|
MD_FILES: dict[str, str] = {
|
||||||
|
"adrian": "Adrian.md",
|
||||||
|
"noname": "Noname.md",
|
||||||
|
"agate": "Agate.md",
|
||||||
|
"notorious-rooster": "NotoriousRooster.md",
|
||||||
|
"ubear": "Ubear.md",
|
||||||
|
"azure": "Azure.md",
|
||||||
|
"starboy": "Starboy.md",
|
||||||
|
"jenni": "Jenni.md",
|
||||||
|
"heart-and-mind": "Heart-and-Mind.md",
|
||||||
|
"beanie": "Beanie.md",
|
||||||
|
"raincloud": "RaincloudTheDragon.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_CHAT_ROOT = os.environ.get(
|
||||||
|
"MIXER_TWITCH_CHAT",
|
||||||
|
r"C:\Users\Nathan\SynologyDrive\YouTube\Streams\MixerTwitch",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _avatar_fingerprint(logo_url: str) -> str | None:
|
||||||
|
"""Stable id for the same uploaded image (ignores 70x70 vs 300x300, etc.)."""
|
||||||
|
if not logo_url:
|
||||||
|
return None
|
||||||
|
path = urlparse(logo_url).path
|
||||||
|
base = re.sub(r"-\d+x\d+\.(png|jpe?g|webp)$", "", path, flags=re.IGNORECASE)
|
||||||
|
return base or path
|
||||||
|
|
||||||
|
|
||||||
|
def _best_logo_url(logo_url: str) -> str:
|
||||||
|
"""Prefer a larger Twitch CDN size for download."""
|
||||||
|
if not logo_url:
|
||||||
|
return logo_url
|
||||||
|
u = logo_url
|
||||||
|
for small, large in (("70x70", "300x300"), ("36x36", "300x300"), ("50x50", "300x300")):
|
||||||
|
if small in u:
|
||||||
|
u = u.replace(small, large)
|
||||||
|
break
|
||||||
|
return u
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_created_at(raw) -> datetime | None:
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
if isinstance(raw, (int, float)):
|
||||||
|
return datetime.utcfromtimestamp(raw)
|
||||||
|
s = str(raw).replace("Z", "+00:00")
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(s)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def open_chat_json(path: Path) -> dict | None:
|
||||||
|
try:
|
||||||
|
with path.open("rb") as f:
|
||||||
|
magic = f.read(4)
|
||||||
|
if magic[:2] == b"\x1f\x8b":
|
||||||
|
with gzip.open(path, "rt", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (OSError, json.JSONDecodeError) as e:
|
||||||
|
print(f"skip {path}: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SeenAvatar:
|
||||||
|
fingerprint: str
|
||||||
|
first_at: datetime
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
def collect_avatars(chat_root: Path) -> dict[tuple[str, str], dict[str, SeenAvatar]]:
|
||||||
|
"""
|
||||||
|
Returns map (slug, login) -> fingerprint -> SeenAvatar (earliest message wins per fp).
|
||||||
|
"""
|
||||||
|
login_to_slug: dict[str, str] = {}
|
||||||
|
for slug, logins in ROSTER.items():
|
||||||
|
for lg in logins:
|
||||||
|
login_to_slug[lg.lower()] = slug
|
||||||
|
|
||||||
|
# (slug, login) -> fp -> SeenAvatar
|
||||||
|
acc: dict[tuple[str, str], dict[str, SeenAvatar]] = defaultdict(dict)
|
||||||
|
|
||||||
|
files = sorted(chat_root.glob("*/chat/*.json"))
|
||||||
|
if not files:
|
||||||
|
print(f"No JSON under {chat_root}/*/chat/", file=sys.stderr)
|
||||||
|
return acc
|
||||||
|
|
||||||
|
for jp in files:
|
||||||
|
data = open_chat_json(jp)
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
for c in data.get("comments") or []:
|
||||||
|
com = c.get("commenter") or {}
|
||||||
|
login = (com.get("name") or com.get("login") or "").strip().lower()
|
||||||
|
if not login or login not in login_to_slug:
|
||||||
|
continue
|
||||||
|
logo = com.get("logo")
|
||||||
|
if not logo or not str(logo).startswith("http"):
|
||||||
|
continue
|
||||||
|
fp = _avatar_fingerprint(str(logo))
|
||||||
|
if not fp:
|
||||||
|
continue
|
||||||
|
slug = login_to_slug[login]
|
||||||
|
key = (slug, login)
|
||||||
|
created = _parse_created_at(c.get("created_at"))
|
||||||
|
if created is None:
|
||||||
|
created = datetime.min
|
||||||
|
cur = acc[key].get(fp)
|
||||||
|
url = _best_logo_url(str(logo))
|
||||||
|
if cur is None or created < cur.first_at:
|
||||||
|
acc[key][fp] = SeenAvatar(fp, created, url)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
|
||||||
|
|
||||||
|
def download_as_jpeg(url: str, dest: Path) -> bool:
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
req = Request(url, headers={"User-Agent": "BattleRoyale-pfp-fetch/1.0"})
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=60) as resp:
|
||||||
|
data = resp.read()
|
||||||
|
except OSError as e:
|
||||||
|
print(f"download failed {url}: {e}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
im = Image.open(BytesIO(data)).convert("RGB")
|
||||||
|
im.save(dest, "JPEG", quality=92)
|
||||||
|
except ImportError:
|
||||||
|
# No Pillow: write raw if already jpeg
|
||||||
|
if data[:2] == b"\xff\xd8":
|
||||||
|
dest.write_bytes(data)
|
||||||
|
else:
|
||||||
|
print("Install Pillow for PNG/WebP -> JPEG: pip install pillow", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
except OSError as e:
|
||||||
|
print(f"jpeg encode {url}: {e}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
SECTION_START = "<!-- pfp:chat-exports -->"
|
||||||
|
SECTION_END = "<!-- /pfp:chat-exports -->"
|
||||||
|
|
||||||
|
|
||||||
|
def build_section_md(images: list[tuple[str, str]]) -> str:
|
||||||
|
"""images: list of (caption, relative_path from Story/)"""
|
||||||
|
lines = [SECTION_START, "", "## Profile pictures", ""]
|
||||||
|
for caption, rp in images:
|
||||||
|
lines.append(f"**{caption}**")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(SECTION_END)
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def inject_section(content: str, section_md: str) -> str:
|
||||||
|
if SECTION_START in content and SECTION_END in content:
|
||||||
|
pre, _, rest = content.partition(SECTION_START)
|
||||||
|
_, _, post = rest.partition(SECTION_END)
|
||||||
|
return pre.rstrip() + "\n\n" + section_md + "\n" + post.lstrip()
|
||||||
|
# After first heading line
|
||||||
|
lines = content.splitlines()
|
||||||
|
if not lines:
|
||||||
|
return section_md + content
|
||||||
|
out = [lines[0], "", section_md]
|
||||||
|
if len(lines) > 1 and lines[1].strip():
|
||||||
|
out.append("")
|
||||||
|
out.extend(lines[1:])
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--chat-root", type=Path, default=Path(DEFAULT_CHAT_ROOT))
|
||||||
|
ap.add_argument("--story-root", type=Path, default=Path(__file__).resolve().parents[1] / "Story")
|
||||||
|
ap.add_argument(
|
||||||
|
"--no-write-md",
|
||||||
|
action="store_true",
|
||||||
|
help="Only download images; do not edit Story/*.md",
|
||||||
|
)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if not args.chat_root.is_dir():
|
||||||
|
print(f"Chat root not found: {args.chat_root}", file=sys.stderr)
|
||||||
|
print("Set MIXER_TWITCH_CHAT or pass --chat-root.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
acc = collect_avatars(args.chat_root)
|
||||||
|
pfp_root = args.story_root / "pfp"
|
||||||
|
|
||||||
|
# Flatten per slug for multi-login: still per-login files
|
||||||
|
for slug, md_name in MD_FILES.items():
|
||||||
|
story_rel = f"pfp/{slug}"
|
||||||
|
section_images: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
display = Path(MD_FILES[slug]).stem.replace("-", " ")
|
||||||
|
|
||||||
|
if slug == "heart-and-mind":
|
||||||
|
order = [("Heart", "heart_cccc"), ("Mind", "brush_colourful")]
|
||||||
|
for label, login in order:
|
||||||
|
key = (slug, login)
|
||||||
|
avs = acc.get(key, {})
|
||||||
|
ordered = sorted(avs.values(), key=lambda x: x.first_at)
|
||||||
|
for i, av in enumerate(ordered):
|
||||||
|
fn = f"{login}_{i}.jpg"
|
||||||
|
rel = f"{story_rel}/{fn}"
|
||||||
|
dest = pfp_root / slug / fn
|
||||||
|
if download_as_jpeg(av.url, dest):
|
||||||
|
cap = f"{label} (`{login}`)"
|
||||||
|
if len(ordered) > 1:
|
||||||
|
cap = f"{cap} — {i + 1}"
|
||||||
|
section_images.append((cap, rel))
|
||||||
|
else:
|
||||||
|
logins = ROSTER[slug]
|
||||||
|
for login in logins:
|
||||||
|
key = (slug, login)
|
||||||
|
avs = acc.get(key, {})
|
||||||
|
ordered = sorted(avs.values(), key=lambda x: x.first_at)
|
||||||
|
if not ordered:
|
||||||
|
continue
|
||||||
|
for i, av in enumerate(ordered):
|
||||||
|
fn = f"{login}_{i}.jpg"
|
||||||
|
rel = f"{story_rel}/{fn}"
|
||||||
|
dest = pfp_root / slug / fn
|
||||||
|
if not download_as_jpeg(av.url, dest):
|
||||||
|
continue
|
||||||
|
if len(logins) > 1:
|
||||||
|
cap = f"`{login}`"
|
||||||
|
else:
|
||||||
|
cap = display
|
||||||
|
if len(ordered) > 1:
|
||||||
|
cap = f"{cap} — {i + 1}"
|
||||||
|
section_images.append((cap, rel))
|
||||||
|
|
||||||
|
if not section_images:
|
||||||
|
print(f"No avatars found for {slug}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
section_md = build_section_md(section_images)
|
||||||
|
md_path = args.story_root / md_name
|
||||||
|
if not args.no_write_md and md_path.is_file():
|
||||||
|
text = md_path.read_text(encoding="utf-8")
|
||||||
|
md_path.write_text(inject_section(text, section_md), encoding="utf-8")
|
||||||
|
print(f"updated {md_path}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user