#!/usr/bin/env bash
#
# fetch-yt-dlp.sh — fetch the pinned official yt-dlp standalone macOS binary Snitt.app ships.
#
# yt-dlp is the downloader behind "Add from YouTube…" (see Pipeline/Tools/YtDlp.swift).
# Unlike ffmpeg there is nothing to build: the yt-dlp project publishes a
# standalone universal2 macOS binary with every release, alongside a
# SHA2-256SUMS file. This script is the reproducible-fetch counterpart of
# build-ffmpeg.sh: pin -> download -> verify SHA-256 -> strip quarantine ->
# sanity-run -> LICENCE GATE -> drop into vendor/.
#
# ############################################################################
# #  THIS BINARY IS NOT PUBLIC DOMAIN. IT IS EFFECTIVELY GPLv3.
# #
# #  yt-dlp's own source is the Unlicense, and this script used to say that was
# #  the whole story. It was wrong, and it was wrong in the shipped LICENSE.md
# #  too, for as long as the binary has shipped. `yt-dlp_macos` is a PyInstaller
# #  ONEFILE build: it freezes a whole CPython, OpenSSL, and yt-dlp's optional
# #  dependencies into one executable. One of those is **mutagen (GPLv2+)**, and
# #  GPLv2+ combined with the Apache-2.0 parts (requests, OpenSSL) is compatible
# #  only at GPLv3. So the artefact we redistribute is a combined work under
# #  GPLv3, and shipping it obliges us to offer its complete corresponding
# #  source — the same obligation the LGPL ffmpeg carries, discharged the same
# #  way (https://download.snitt.ai/source/).
# #
# #  It does NOT make Snitt GPL: the app spawns this binary as a subprocess and
# #  talks to it over argv and a pipe, which is aggregation, not linking.
# #
# #  `verify_licence_composition` below is the gate that stops this drifting
# #  again. It asks the freshly downloaded binary what is frozen into it and
# #  fails if the answer is not the list we audited. A version bump that adds a
# #  component is a LICENCE REVIEW, not a packaging detail.
# #
# #  NEVER swap this asset for `yt-dlp_macos.zip` or any other PyInstaller
# #  variant without re-running that review, and never restore the claim that
# #  this binary is "Unlicense (public domain)".
# ############################################################################
#
# Output: frontend/Snitt/vendor/yt-dlp/yt-dlp     (gitignored — ~37 MB)
#         frontend/Snitt/vendor/yt-dlp/VERSION    (gitignored, provenance)
# The committed LICENSE.md beside them states the real composition and ships in
# the bundle. The Xcode "Bundle yt-dlp" script phase copies all three into the
# .app and signs the binary. A missing binary is a warning there, not an error,
# so a fresh clone still builds and the app falls back to a PATH/Homebrew yt-dlp.
#
# Usage:  scripts/fetch-yt-dlp.sh

set -euo pipefail

# ---------------------------------------------------------------------------
# The pin. Bumping yt-dlp = change these two lines and re-run. The SHA-256 is
# the yt-dlp_macos line of the release's own SHA2-256SUMS file:
#   https://github.com/yt-dlp/yt-dlp/releases/download/<version>/SHA2-256SUMS
# ---------------------------------------------------------------------------
YTDLP_VERSION="2026.07.04"
YTDLP_SHA256="498bd0dae17855c599d371d68ec5bafc439a9d8640e838be25c765a9792f261b"
YTDLP_URL="https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp_macos"

# ---------------------------------------------------------------------------
# The audited licence composition, as reported by `yt-dlp -v` for the pin above
# (measured 2026-08-22). This is the list the shipped LICENSE.md documents and
# a lawyer would be shown; `verify_licence_composition` fails if the binary
# reports anything outside it.
#
# Names only — versions drift within a pin's lifetime without changing anyone's
# licence, and pinning them here would make the gate cry wolf on every bump.
# What matters is that no NEW component appears unreviewed.
# ---------------------------------------------------------------------------
AUDITED_COMPONENTS=(
  Cryptodome    # BSD 2-Clause / public domain
  brotli        # MIT
  certifi       # MPL 2.0
  curl_cffi     # MIT (bundles libcurl, curl licence)
  mutagen       # GPL 2.0-or-later  <- the one that makes the whole thing GPLv3
  requests      # Apache 2.0
  sqlite3       # public domain (CPython stdlib)
  urllib3       # MIT
  websockets    # BSD 3-Clause
  yt_dlp_ejs    # Unlicense
)

# The component whose presence is the entire reason this binary is GPL. If it
# ever disappears, that is not a quiet win to pocket — it means the upstream
# build changed shape, and the licence story (and LICENSE.md) must be redone.
COPYLEFT_COMPONENT="mutagen"

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEST_DIR="${REPO_ROOT}/frontend/Snitt/vendor/yt-dlp"
WORK_DIR="${REPO_ROOT}/.build/yt-dlp"

log()  { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }

command -v curl >/dev/null || die "curl not found"

# --- 0. the licence guard ---------------------------------------------------
# Mirrors build-ffmpeg.sh's `verify_built_binaries`: interrogate the artefact
# that will actually ship, and refuse to install it if its licence story is not
# the one we audited. There it reads `ffmpeg -L`; here it reads the "Optional
# libraries" line of `yt-dlp -v`, which is the binary's own inventory of what
# PyInstaller froze into it.
#
# The absence of this check is why a GPLv2+ dependency shipped for months
# behind a LICENSE.md that said "Unlicense (public domain)".
verify_licence_composition() {
  local binary="$1" line reported unexpected=() missing=()

  # `-v` with no URL prints the debug header and then exits non-zero ("You must
  # provide at least one URL"), which is fine: we only want the header.
  line="$("$binary" -v 2>&1 | grep -i '^\[debug\] Optional libraries:' || true)"
  [[ -n "$line" ]] \
    || die "could not read the 'Optional libraries' line from '${binary} -v'.
Without it the licence composition cannot be verified, and an unverified
composition is exactly how mutagen (GPLv2+) shipped under a LICENSE.md that
claimed the Unlicense. Refusing to install."

  # "Cryptodome-3.23.0, brotli-1.2.0, …" -> bare names, one per line.
  reported="$(printf '%s\n' "$line" \
    | sed -e 's/^.*Optional libraries: *//' -e 's/, */\n/g' \
    | sed -e 's/-[0-9].*$//' -e '/^$/d' | sort -u)"

  local component
  for component in $reported; do
    local known=0 audited
    for audited in "${AUDITED_COMPONENTS[@]}"; do
      [[ "$component" == "$audited" ]] && { known=1; break; }
    done
    [[ "$known" -eq 1 ]] || unexpected+=("$component")
  done
  for audited in "${AUDITED_COMPONENTS[@]}"; do
    printf '%s\n' "$reported" | grep -qx "$audited" || missing+=("$audited")
  done

  if [[ ${#unexpected[@]} -gt 0 ]]; then
    die "UNAUDITED COMPONENT(S) FROZEN INTO yt-dlp ${YTDLP_VERSION}: ${unexpected[*]}

This binary ships inside Snitt.app, so every component in it is something we
redistribute and must be able to name a licence for. One of them has appeared
since the last audit.

Do this, in order:
  1. Find each component's licence (PyPI / its repository).
  2. Decide whether it changes the effective licence of the combined work —
     today that is GPLv3, forced by ${COPYLEFT_COMPONENT}. Anything more
     restrictive (AGPL, a non-commercial or source-only licence) means this
     binary CANNOT SHIP AT ALL; stop and escalate.
  3. Update the table in frontend/Snitt/vendor/yt-dlp/LICENSE.md.
  4. Add the component to AUDITED_COMPONENTS in this script.
  5. Make sure https://download.snitt.ai/source/ carries its source too."
  fi

  if [[ ${#missing[@]} -gt 0 ]]; then
    die "AUDITED COMPONENT(S) NO LONGER IN yt-dlp ${YTDLP_VERSION}: ${missing[*]}

The upstream build changed shape. This is not automatically good news: the
shipped LICENSE.md documents a composition that no longer matches the binary,
which is the same class of inaccuracy this gate exists to prevent.

Re-audit as above, then prune AUDITED_COMPONENTS and LICENSE.md to match."
  fi

  printf '%s\n' "$reported" | grep -qx "$COPYLEFT_COMPONENT" \
    || die "${COPYLEFT_COMPONENT} is gone from yt-dlp ${YTDLP_VERSION}.

It is the component that makes this binary GPLv3, and LICENSE.md is written
around it. Its absence may mean the binary is now redistributable under much
friendlier terms — verify that properly and rewrite LICENSE.md before shipping,
rather than shipping a notice that overstates our obligations."

  log "licence gate: composition matches the audit ($(printf '%s' "$reported" | wc -w | tr -d ' ') components, ${COPYLEFT_COMPONENT} present ⇒ GPLv3)"
}

# --- 1. fetch ---------------------------------------------------------------
BINARY="${WORK_DIR}/yt-dlp_macos-${YTDLP_VERSION}"
mkdir -p "$WORK_DIR"
if [[ -f "$BINARY" ]]; then
  log "already downloaded: $(basename "$BINARY")"
else
  log "fetching ${YTDLP_URL}"
  curl -fL --retry 3 --output "${BINARY}.partial" "$YTDLP_URL"
  mv "${BINARY}.partial" "$BINARY"
fi

# --- 2. verify --------------------------------------------------------------
# A hard stop, not a warning: this binary gets signed with the app's identity
# and shipped. An unverified download is the one way a tampered payload could
# ride along.
ACTUAL="$(shasum -a 256 "$BINARY" | awk '{print $1}')"
if [[ "$ACTUAL" != "$YTDLP_SHA256" ]]; then
  die "SHA-256 mismatch for yt-dlp_macos ${YTDLP_VERSION}
  expected: ${YTDLP_SHA256}
  actual:   ${ACTUAL}
Refusing to install. Either the download is corrupt or the pin is wrong."
fi
log "SHA-256 verified"

# --- 3. make runnable -------------------------------------------------------
# Browsers quarantine downloads; curl usually doesn't, but stripping the
# attribute is free and its presence would make Gatekeeper refuse the spawn.
xattr -d com.apple.quarantine "$BINARY" 2>/dev/null || true
chmod 755 "$BINARY"

ARCHS="$(lipo -archs "$BINARY" 2>/dev/null || echo "unknown")"
case "$ARCHS" in
  *arm64*x86_64*|*x86_64*arm64*) ;;
  *) warn "expected a universal2 binary, got archs: ${ARCHS}" ;;
esac

REPORTED="$("$BINARY" --version 2>/dev/null | tail -1 || true)"
[[ "$REPORTED" == "$YTDLP_VERSION" ]] \
  || die "binary reports version '${REPORTED}', expected '${YTDLP_VERSION}'"

# --- 3.5 licence gate -------------------------------------------------------
# Runs against the downloaded binary, before it is copied into vendor/ — so a
# composition we have not audited never reaches the tree the build phase
# bundles from.
verify_licence_composition "$BINARY"

# --- 4. install -------------------------------------------------------------
mkdir -p "$DEST_DIR"
cp -f "$BINARY" "${DEST_DIR}/yt-dlp"
chmod 755 "${DEST_DIR}/yt-dlp"

# Line one is the "tool version" shape BundledTools-style parsers expect.
cat > "${DEST_DIR}/VERSION" <<EOF
yt-dlp ${YTDLP_VERSION}
source   ${YTDLP_URL}
sha256   ${YTDLP_SHA256}
fetched  $(date -u '+%Y-%m-%dT%H:%M:%SZ')
archs    ${ARCHS}
licence  GPL-3.0 as a combined work (yt-dlp itself is Unlicense; the PyInstaller
         onefile freezes in mutagen GPL-2.0-or-later, CPython, OpenSSL and more
         — see LICENSE.md beside this file for the full component table)
source   https://download.snitt.ai/source/
recipe   scripts/fetch-yt-dlp.sh
EOF

log "yt-dlp ${YTDLP_VERSION} (${ARCHS}) installed in ${DEST_DIR} — rebuild the app to bundle it"
