#!/usr/bin/env bash
# Share a local model folder on Pirate Face in one go: builds a v1 torrent,
# checks the files against Hugging Face, and submits the magnet. Your files
# are only read, never changed or uploaded.
#
#   curl -fsSL https://pirateface.co/package.sh | bash -s -- --repo author/model ./model-folder
#
# Or share models you already downloaded with Hugging Face tools. This finds
# them in the Hugging Face cache, shows what can be shared, then asks per model:
#
#   curl -fsSL https://pirateface.co/package.sh | bash -s -- --hf-cache
#
# Needs: mktorrent, python3, curl, sha256sum (or shasum).
# Env: PIRATEFACE_KEY (else asked once and saved), HF_TOKEN for gated repos.
# Torrent client: TR_HOST, TR_AUTH=user:pass (Transmission); QBT_URL, QBT_USER, QBT_PASS (qBittorrent).
# -y / --yes skips the prompts; only for agents acting on the user's explicit request.
set -euo pipefail
# Braces make bash read the whole script before running it. Piped from curl, an
# early exit would otherwise close the pipe and curl would print error 23.
{

SITE="${PIRATEFACE_URL:-https://pirateface.co}"
TRACKERS=(udp://tracker.pirateface.co:6969/announce http://tracker.pirateface.co:6969/announce)
TERMS_VERSION="2026-09-18.2"
KEY_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/pirateface/key"
REPO="" REVISION="" OUT="" YES="" NAME="" HF_CACHE=""

usage() {
  echo "usage: package.sh --repo author/model [--revision COMMIT] [--name NAME] [--out DIR] [-y] ./model-folder"
  echo "       package.sh --hf-cache [-y]"
  exit "${1:-0}"
}
die() { echo "error: $*" >&2; exit 1; }
# Prompts read the keyboard, not stdin, so `curl | bash` still works.
ask() { [ -n "$YES" ] && return 0; [ -r /dev/tty ] || die "no terminal for prompts (agents: pass -y with the user's OK)"; read -r "$@" </dev/tty; }

while [ $# -gt 0 ]; do
  case "$1" in
    --repo) REPO="${2:?}"; shift 2 ;;
    --revision) REVISION="${2:?}"; shift 2 ;;
    --out) OUT="${2:?}"; shift 2 ;;
    --name) NAME="${2:?}"; shift 2 ;;
    --hf-cache) HF_CACHE=1; shift ;;
    -y|--yes) YES=1; shift ;;
    -h|--help) usage ;;
    -*) die "unknown option $1" ;;
    *) [ -z "${SRC:-}" ] || die "only one folder"; SRC="$1"; shift ;;
  esac
done

# --hf-cache: list every model in the Hugging Face cache, then package the ones
# Pirate Face does not have yet. The cache names large files by their SHA-256,
# so matching them against Hugging Face needs no hashing.
if [ -n "$HF_CACHE" ]; then
  command -v python3 >/dev/null || die "python3 not found"
  HUB="${HF_HUB_CACHE:-${HF_HOME:-$HOME/.cache/huggingface}/hub}"
  [ -d "$HUB" ] || die "no Hugging Face cache at $HUB (set HF_HUB_CACHE)"
  echo "==> Checking models in $HUB"
  PLAN="$(mktemp)"; trap 'rm -f "$PLAN"' EXIT
  python3 - "$HUB" "$SITE" > "$PLAN" <<'PY'
import glob, json, os, re, sys, urllib.error, urllib.parse, urllib.request
hub, site = sys.argv[1:3]
def get(url, as_json=True):
    req = urllib.request.Request(url, headers={"User-Agent": "pirateface-package.sh"})
    if "huggingface.co" in url and os.environ.get("HF_TOKEN"):
        req.add_header("Authorization", "Bearer " + os.environ["HF_TOKEN"])
    with urllib.request.urlopen(req, timeout=30) as r:
        nxt = re.search(r'<([^>]+)>;\s*rel="next"', r.headers.get("Link") or "")
        return (json.load(r) if as_json else r.read().decode()), nxt and nxt.group(1)
def hidden(path): return any(part.startswith(".") for part in path.split("/"))
def gb(n): return f"{n / 1e9:.1f} GB" if n >= 1e8 else f"{n / 1e6:.0f} MB"
for snap in sorted(glob.glob(os.path.join(hub, "models--*", "snapshots", "*"))):
    repo = os.path.basename(os.path.dirname(os.path.dirname(snap)))[len("models--"):].replace("--", "/", 1)
    rev = os.path.basename(snap)
    row = lambda status, why, size="-", magnet="-": print("\t".join([status, repo, rev, snap, size, why, magnet]))
    try:
        info = get(f"https://huggingface.co/api/models/{repo}/revision/{rev}")[0]
        url, remote = f"https://huggingface.co/api/models/{repo}/tree/{rev}?recursive=true", {}
        while url:
            page, url = get(url)
            remote.update({e["path"]: e for e in page if e.get("type") == "file" and not hidden(e["path"])})
    except Exception:
        row("skip", "not readable on Hugging Face (gated? set HF_TOKEN)"); continue
    raw = (info.get("cardData") or {}).get("license")
    lics = [str(x).strip().lower() for x in (raw if isinstance(raw, list) else [raw]) if str(x or "").strip()]
    lic = next((x for x in ("mit", "apache-2.0") if x in lics), lics[0] if lics else "none")
    size = gb(sum((e.get("lfs") or e).get("size", 0) for e in remote.values()))
    if lic not in ("mit", "apache-2.0") and repo.lower() != "moonshotai/kimi-k3":
        row("skip", f"license {lic}", size); continue
    have = missing = bad = 0
    for path, e in remote.items():
        local = os.path.join(snap, path)
        if not os.path.exists(local):
            missing += bool(e.get("lfs"))  # weights must all be here; a skipped README is fine
            continue
        want = (e.get("lfs") or {}).get("oid") or e.get("oid")
        if os.path.basename(os.path.realpath(local)) != want: bad += 1
        else: have += 1
    if bad: row("skip", f"{bad} files differ from Hugging Face", size); continue
    if missing: row("skip", f"partial download: {missing} weight files missing", size); continue
    try:
        magnet = get(f"{site}/api/torrents?repo={urllib.parse.quote(repo, safe='/')}")[0].get("magnet")
    except urllib.error.HTTPError as e:
        if e.code != 404:
            row("skip", f"Pirate Face lookup failed (HTTP {e.code})", size); continue
        magnet = None  # not listed yet
    except Exception:
        row("skip", "could not reach Pirate Face", size); continue
    if magnet: row("listed", "already on Pirate Face", size, magnet)
    else: row("submit", "complete, not on Pirate Face yet", size)
PY
  echo
  while IFS=$'\t' read -r status repo rev snap size why magnet; do
    printf '    %-7s %-48s %9s  %s\n' "$status" "$repo" "$size" "$why"
  done < "$PLAN"
  COUNT=$(grep -c '^submit' "$PLAN" || true)
  [ "$COUNT" -gt 0 ] || { echo; echo "    Nothing new to submit."; exit 0; }
  # Re-run this script per model. Piped through curl there is no file, so fetch it.
  SELF="$0"
  if ! grep -q 'pirateface' "$SELF" 2>/dev/null; then
    SELF="$(mktemp)"; trap 'rm -f "$PLAN" "$SELF"' EXIT
    curl -fsSL "$SITE/package.sh" -o "$SELF" || die "could not download $SITE/package.sh"
  fi
  SEEDS="${PIRATEFACE_SEEDS:-$HOME/pirateface-seeds}"
  # Models that could not be added to a client are collected here and listed at the end.
  export PF_NEEDS_FILE="$(mktemp)"; FAILED=() LATER=()
  # Pirate Face takes 5 submissions an hour per account. Build at most 5 per run
  # so nobody hashes hundreds of GB only to be turned away.
  TRIES=0
  while IFS=$'\t' read -r status repo rev snap size why magnet <&3; do
    [ "$status" = submit ] || continue
    [ "$TRIES" -lt 5 ] || { LATER+=("$repo"); continue; }
    echo
    if [ -z "$YES" ]; then ask -p "==> Share $repo ($size)? [y/N] " OK; [[ "${OK:-}" =~ ^[Yy] ]] || continue; fi
    name="${repo#*/}"; TRIES=$((TRIES + 1)); RC=0
    bash "$SELF" --repo "$repo" --revision "$rev" --name "$name" --out "$SEEDS/$name" ${YES:+-y} "$snap" || RC=$?
    if [ "$RC" -eq 75 ]; then LATER+=("$repo"); TRIES=5; continue; fi
    if [ "$RC" -ne 0 ]; then
      grep -q "^$repo	" "$PF_NEEDS_FILE" || FAILED+=("$repo")
      echo "    $repo not submitted; continuing"
    fi
  done 3< "$PLAN"
  if [ -s "$PF_NEEDS_FILE" ]; then
    echo; echo "==> Needs you: no torrent client could be reached for these. Nothing was submitted."
    echo "    Open each .torrent in your torrent client and save to the folder shown. Then run"
    echo "    this again without -y and press Enter when each one says Seeding."
    while IFS=$'\t' read -r repo torrent dir; do
      echo "    $repo"; echo "        open:    $torrent"; echo "        save to: $dir"
    done < "$PF_NEEDS_FILE"
  fi
  if [ ${#FAILED[@]} -gt 0 ]; then
    echo; echo "==> Not submitted (see the error above each one): ${FAILED[*]}"
  fi
  if [ ${#LATER[@]} -gt 0 ]; then
    echo; echo "==> Next hour: Pirate Face takes 5 submissions an hour. Run this again in an hour for: ${LATER[*]}"
  fi
  rm -f "$PF_NEEDS_FILE"
  exit 0
fi

[ -n "${SRC:-}" ] && [ -n "$REPO" ] || usage 1
[ -d "$SRC" ] || die "$SRC is not a folder"
[[ "$REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || die "--repo must look like author/model"
for c in mktorrent python3 curl; do command -v "$c" >/dev/null || die "$c not found (apt/brew install $c)"; done
if command -v sha256sum >/dev/null; then SHA=(sha256sum); else SHA=(shasum -a 256); fi
[ -z "$REVISION" ] || [[ "$REVISION" =~ ^[0-9a-fA-F]{40}$ ]] || die "--revision must be a full 40-character commit ID"

SRC="$(cd "$SRC" && pwd)"
NAME="${NAME:-$(basename "$SRC")}"
[[ "$NAME" =~ ^[A-Za-z0-9._-]+$ ]] || die "--name may only use letters, numbers, . _ -"
OUT="${OUT:-$PWD/pirateface-$NAME}"
mkdir -p "$OUT"
STAGE="$(mktemp -d)"; trap 'rm -rf "$STAGE"' EXIT
mkdir "$STAGE/$NAME"

# 1. Stage only model files as symlinks. Hidden paths (.git, .cache, .env),
#    keys and partial downloads stay out of the torrent.
echo "==> Selecting files in $SRC"
EXCLUDED=0
while IFS= read -r -d '' f; do
  rel="${f#"$SRC"/}"
  case "/$rel" in
    */.*|*/__pycache__/*|*.pem|*.key|*/id_rsa*|*.incomplete|*.lock|*.part)
      echo "    skip  $rel"; EXCLUDED=$((EXCLUDED + 1)); continue ;;
  esac
  mkdir -p "$STAGE/$NAME/$(dirname "$rel")"
  ln -s "$f" "$STAGE/$NAME/$rel"
done < <(find -L "$SRC" -type f -print0 | sort -z)
COUNT=$(find "$STAGE/$NAME" -type l | wc -l | tr -d ' ')
[ "$COUNT" -gt 0 ] || die "no files left to package"
echo "    $COUNT files included, $EXCLUDED skipped"
# The client looks for files under <save folder>/$NAME. When the folder has a
# different name (the Hugging Face cache names it by commit), keep a folder of
# links with the right name next to the torrent and seed from there.
SAVE_DIR="$(dirname "$SRC")"
if [ "$NAME" != "$(basename "$SRC")" ]; then
  SAVE_DIR="$OUT/files"
  rm -rf "$SAVE_DIR/$NAME"; mkdir -p "$SAVE_DIR"; cp -a "$STAGE/$NAME" "$SAVE_DIR/"
fi

# 2. Checksums, paths relative to the torrent root.
echo "==> Hashing (SHA-256)"
(cd "$STAGE/$NAME" && find . -type l -print0 | sort -z | xargs -0 -n 8 -P 4 "${SHA[@]}") \
  | sed 's#  \./#  #' | sort -k2 > "$OUT/checksums.txt"

# 3. BitTorrent v1 torrent with the Pirate Face trackers. ~2000 pieces, 256 KiB-16 MiB.
PIECE=$(python3 - "$STAGE/$NAME" <<'PY'
import os, sys
total = sum(os.path.getsize(os.path.join(d, f)) for d, _, fs in os.walk(sys.argv[1], followlinks=True) for f in fs)
n = 18
while n < 24 and total / (1 << n) > 2000: n += 1
print(n)
PY
)
echo "==> Creating torrent (piece size 2^$PIECE)"
ANNOUNCE=(); for t in "${TRACKERS[@]}"; do ANNOUNCE+=(-a "$t"); done
rm -f "$OUT/$NAME.torrent"
mktorrent "${ANNOUNCE[@]}" -l "$PIECE" -n "$NAME" -o "$OUT/$NAME.torrent" "$STAGE/$NAME" >/dev/null

INFOHASH=$(python3 - "$OUT/$NAME.torrent" <<'PY'
import hashlib, sys
data = open(sys.argv[1], "rb").read()
def skip(i):
    c = data[i:i+1]
    if c == b"i": return data.index(b"e", i) + 1
    if c in b"ld":
        i += 1
        while data[i:i+1] != b"e": i = skip(i)
        return i + 1
    colon = data.index(b":", i)
    return colon + 1 + int(data[i:colon])
i = 1
while data[i:i+1] != b"e":
    key_end = skip(i); key = data[data.index(b":", i) + 1:key_end]; val_end = skip(key_end)
    if key == b"info": print(hashlib.sha1(data[key_end:val_end]).hexdigest()); break
    i = val_end
PY
)
MAGNET="magnet:?xt=urn:btih:$INFOHASH"
echo "$MAGNET" > "$OUT/magnet.txt"
# Trackers stay out of the submitted magnet (the site adds them back). They
# belong on the link the user opens, or the client never announces.
SEED_MAGNET=$(python3 - "$INFOHASH" "$NAME" "${TRACKERS[@]}" <<'PY'
import sys, urllib.parse
ih, name, *trackers = sys.argv[1:]
q = urllib.parse.quote
print("magnet:?xt=urn:btih:" + ih + "&dn=" + q(name, safe="") + "".join("&tr=" + q(t, safe="") for t in trackers))
PY
)

# 4. Compare against Hugging Face LFS hashes and read the license. If every hash
#    matches, the submission is listed right away instead of going to review.
echo "==> Checking against huggingface.co/$REPO"
read -r HF_SHA HF_LICENSE HF_MATCH < <(python3 - "$REPO" "${REVISION:-main}" "$OUT/checksums.txt" <<'PY'
import json, os, re, sys, urllib.request
repo, rev, manifest = sys.argv[1:4]
def get(url):
    req = urllib.request.Request(url)
    if os.environ.get("HF_TOKEN"): req.add_header("Authorization", "Bearer " + os.environ["HF_TOKEN"])
    with urllib.request.urlopen(req, timeout=30) as r:
        nxt = re.search(r'<([^>]+)>;\s*rel="next"', r.headers.get("Link") or "")
        return json.load(r), nxt and nxt.group(1)
try:
    info = get(f"https://huggingface.co/api/models/{repo}/revision/{rev}")[0]
    sha = info["sha"]
    raw = (info.get("cardData") or {}).get("license")
    items = raw if isinstance(raw, list) else [raw]
    items = [str(x).strip().lower() for x in items if str(x or "").strip()]
    lic = next((x for x in ("mit", "apache-2.0") if x in items), items[0] if items else "-")
    url, remote = f"https://huggingface.co/api/models/{repo}/tree/{sha}?recursive=true", {}
    while url:
        page, url = get(url)
        remote.update({e["path"]: e["lfs"]["oid"] for e in page if e.get("lfs")})
except Exception as e:
    print(f"    could not read it from Hugging Face ({e}); the review team will check by hand", file=sys.stderr)
    print("- - no"); sys.exit(0)
local = dict(reversed(l.rstrip("\n").split("  ", 1)) for l in open(manifest))
bad = [p for p in remote if p in local and local[p] != remote[p]]
missing = [p for p in remote if p not in local]
print(f"    revision {sha}: {len(remote) - len(bad) - len(missing)}/{len(remote)} LFS files match, license {lic}", file=sys.stderr)
for p in bad: print(f"    MISMATCH  {p}", file=sys.stderr)
for p in missing: print(f"    not in torrent  {p}", file=sys.stderr)
print(sha, lic, "yes" if remote and not bad else "no")
PY
)
if [ "$HF_MATCH" = yes ]; then
  REVISION="$HF_SHA"
elif [ -z "$REVISION" ]; then
  echo "    Hashes don't match Hugging Face, so it goes to manual review."
  ask -p "    Commit ID your copy came from (40 characters): " REVISION
  [[ "${REVISION:-}" =~ ^[0-9a-fA-F]{40}$ ]] || die "need the full 40-character commit ID; rerun with --revision"
fi

case "$REPO:$HF_LICENSE" in
  [Mm]oonshotai/[Kk]imi-[Kk]3:*) LICENSE=other ;;
  *:mit|*:apache-2.0) LICENSE="$HF_LICENSE" ;;
  *:-) ask -p "    License (mit or apache-2.0): " LICENSE
       [[ "${LICENSE:-}" =~ ^(mit|apache-2.0)$ ]] || die "Pirate Face lists MIT and Apache-2.0 models only: $SITE/policy" ;;
  *) die "license is $HF_LICENSE; Pirate Face lists MIT and Apache-2.0 models only: $SITE/policy" ;;
esac

if [ "$HF_MATCH" = yes ]; then
  EVIDENCE="Source: https://huggingface.co/$REPO at revision $REVISION. License per model card: $LICENSE. All Hugging Face LFS SHA-256 hashes at that revision match the local copy (checked by package.sh)."
else
  echo "    Where did this copy come from? Links, license proof, and why it's gone from Hugging Face if it is."
  ask -p "    Evidence: " EVIDENCE
  [ "${#EVIDENCE}" -ge 30 ] || die "evidence needs at least 30 characters"
fi

# 5. Seed first: a listed magnet is only useful if someone is sharing the files.
#    Add the torrent to a running Transmission or qBittorrent when one answers.
#    Otherwise print the magnet on its own line, wrapped as a terminal link, so a
#    click opens the torrent client.
add_to_client() {
  local torrent="$1" dir="$2" hash="$3" jar tr=(transmission-remote ${TR_HOST:-})
  [ -z "${TR_AUTH:-}" ] || tr+=(-n "$TR_AUTH")
  if command -v transmission-remote >/dev/null && "${tr[@]}" -l >/dev/null 2>&1; then
    if "${tr[@]}" -t "$hash" -i 2>/dev/null | grep -q "$hash"; then echo "Transmission (already there)"; return 0; fi
    "${tr[@]}" -w "$dir" -a "$torrent" >/dev/null && { echo Transmission; return 0; }
  fi
  # qBittorrent Web UI. No login needed when "Bypass authentication for clients on localhost" is on.
  local qbt="${QBT_URL:-http://localhost:8080}"
  jar="$(mktemp)"
  if [ -n "${QBT_USER:-}" ]; then
    curl -fsS -c "$jar" -H "Referer: $qbt" --data-urlencode "username=$QBT_USER" \
      --data-urlencode "password=${QBT_PASS:-}" "$qbt/api/v2/auth/login" >/dev/null 2>&1 || true
  fi
  if curl -fsS -b "$jar" "$qbt/api/v2/torrents/info?hashes=$hash" 2>/dev/null | grep -q "$hash"; then
    rm -f "$jar"; echo "qBittorrent (already there)"; return 0
  fi
  if curl -fsS -b "$jar" "$qbt/api/v2/app/version" >/dev/null 2>&1 \
    && curl -fsS -b "$jar" -F "torrents=@$torrent" -F "savepath=$dir" "$qbt/api/v2/torrents/add" >/dev/null 2>&1; then
    rm -f "$jar"; echo qBittorrent; return 0
  fi
  rm -f "$jar"; return 1
}

echo; echo "==> Start seeding"
if CLIENT="$(add_to_client "$OUT/$NAME.torrent" "$SAVE_DIR" "$INFOHASH")"; then
  case "$CLIENT" in
    *already*) echo "    Already in ${CLIENT% (*}." ;;
    *) echo "    Added to $CLIENT with the files in $SAVE_DIR. It checks them, then seeds." ;;
  esac
  echo "    Keep it running. BitTorrent shows your IP address to other peers."
else
  if [ -n "$YES" ] && [ -n "${PF_NEEDS_FILE:-}" ]; then printf '%s\t%s\t%s\n' "$REPO" "$OUT/$NAME.torrent" "$SAVE_DIR" >> "$PF_NEEDS_FILE"; fi
  [ -z "$YES" ] || die "no torrent client answered (Transmission, or qBittorrent Web UI at ${QBT_URL:-http://localhost:8080}). Open $OUT/$NAME.torrent, save to $SAVE_DIR, then rerun without -y."
  cat <<EOF
    Command-click the magnet (Control-click on Linux). Your torrent client opens.
    Save the files to $SAVE_DIR so it finds them and starts seeding.
    Keep it running. BitTorrent shows your IP address to other peers.
EOF
  printf '\033]8;;%s\a%s\033]8;;\a\n' "$SEED_MAGNET" "$SEED_MAGNET"
  echo "    Or open the file: $OUT/$NAME.torrent"
  ask -p "    Press Enter once it says Seeding. " _
fi

# 6. Account key from $SITE/account, saved for next time.
KEY="${PIRATEFACE_KEY:-$(cat "$KEY_FILE" 2>/dev/null || true)}"
if [ -z "$KEY" ]; then
  [ -z "$YES" ] || die "set PIRATEFACE_KEY (from $SITE/account)"
  echo; echo "==> Your Pirate Face key: $SITE/account -> Community torrent key"
  ask -s -p "    Paste it (hidden): " KEY; echo
  [[ "$KEY" == pfct_* ]] || die "that doesn't look like a Pirate Face key (starts with pfct_)"
  mkdir -p "$(dirname "$KEY_FILE")"; (umask 077; echo "$KEY" > "$KEY_FILE")
  echo "    Saved to $KEY_FILE"
fi

# 7. The same five statements as the web form, agreed to once.
cat <<EOF

==> Submitting $REPO ($LICENSE, $COUNT files) to $SITE. By typing yes you confirm:
    1. You have the complete files and the right to redistribute them.
    2. The repository, revision, license, checksums and evidence are accurate, and
       the files are not malware, mislabeled or tampered with. You take responsibility.
    3. Pirate Face may review, reject or remove it and respond to takedowns.
    4. BitTorrent shows your IP to peers, and the torrent has no secrets or personal info.
    5. You agree to the Terms ($SITE/terms#model-torrent-submissions) and Listing policy ($SITE/policy).
EOF
if [ -z "$YES" ]; then
  ask -p "    Type yes: " OK
  [ "${OK:-}" = yes ] || die "not submitted. Your torrent is in $OUT; rerun any time"
fi

BODY="$STAGE/body.json"
python3 - "$BODY" "$REPO" "$REVISION" "$MAGNET" "$LICENSE" "$EVIDENCE" "$OUT/checksums.txt" "$TERMS_VERSION" <<'PY'
import json, sys
out, repo, rev, magnet, lic, evidence, manifest, terms = sys.argv[1:9]
files = [l.rstrip("\n").split("  ", 1) for l in open(manifest)]
json.dump({"repoId": repo, "revision": rev, "magnet": magnet, "license": lic, "evidence": evidence,
  "manifest": [{"path": p, "sha256": h} for h, p in files],
  **dict.fromkeys(["redistributionConfirmed", "accuracyConfirmed", "reviewAcknowledged", "privacyAcknowledged", "termsAccepted"], True),
  "submissionTermsVersion": terms}, open(out, "w"))
PY
CODE=$(curl -sS -o "$STAGE/resp.json" -w '%{http_code}' -X POST "$SITE/api/community-torrents" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" --data-binary "@$BODY") || die "could not reach $SITE"
RESULT=$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("status") or d.get("error") or d)' "$STAGE/resp.json" 2>/dev/null || cat "$STAGE/resp.json")

echo
case "$CODE:$RESULT" in
  200:approved) echo "Listed! $SITE/$REPO" ;;
  200:pending) echo "Submitted. It's in the review queue; you'll see it on $SITE/$REPO once approved." ;;
  409:*) echo "Already on Pirate Face: $SITE/$REPO" ;;  # same torrent submitted before; keep seeding
  401:*) rm -f "$KEY_FILE"; die "key rejected. Get a new one at $SITE/account and rerun" ;;
  429:*) echo "error: $RESULT" >&2; exit 75 ;;  # --hf-cache stops on this code
  *) die "HTTP $CODE: $RESULT" ;;
esac
echo "Keep seeding. Your files: $OUT"
}
