#!/usr/bin/env bash set -u ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" CONFIG_DIR="${FFX_CONFIG_DIR:-${HOME}/.local/etc}" CONFIG_FILE="${FFX_CONFIG_FILE:-${CONFIG_DIR}/ffx.json}" VAR_DIR="${FFX_VAR_DIR:-${HOME}/.local/var/ffx}" LOG_DIR="${FFX_LOG_DIR:-${HOME}/.local/var/log}" DATABASE_FILE="${FFX_DATABASE_FILE:-${VAR_DIR}/ffx.db}" SUBTITLES_BASE_DIR="${FFX_SUBTITLES_BASE_DIR:-${HOME}/.local/var/sync/subtitles}" FFX_PYTHON="${FFX_PYTHON:-${HOME}/.local/share/ffx.venv/bin/python}" CONFIG_TEMPLATE_FILE="${FFX_CONFIG_TEMPLATE:-${ROOT_DIR}/assets/ffx.json.j2}" CHECK_ONLY=0 WITH_TESTS=0 MUTATIONS=0 INSTALL_FAILURES=0 READINESS_FAILURES=0 MISSING_REQUIRED_SYSTEM=() MISSING_OPTIONAL_SYSTEM=() COLOR_RESET="" COLOR_GREEN="" COLOR_YELLOW="" COLOR_RED="" if [ -t 1 ]; then COLOR_RESET="$(printf '\033[0m')" COLOR_GREEN="$(printf '\033[32m')" COLOR_YELLOW="$(printf '\033[33m')" COLOR_RED="$(printf '\033[31m')" fi usage() { cat </dev/null 2>&1 } check_command_component() { command_exists "$2" } check_tmdb_key() { [ -n "${TMDB_API_KEY:-}" ] } check_seeded_dir() { [ -d "$1" ] } check_seeded_file() { [ -f "$1" ] } component_detail() { case "$1" in git|python3|ffmpeg|ffprobe|cpulimit) command -v "$1" || printf "command '%s' not found" "$1" ;; tmdb-key) if check_tmdb_key; then printf 'TMDB_API_KEY is set' else printf 'TMDB_API_KEY is unset; TMDB-backed flows will be skipped or fail' fi ;; config-dir) if check_seeded_dir "${CONFIG_DIR}"; then printf '%s' "${CONFIG_DIR}" else printf 'missing; prep can create it' fi ;; var-dir) if check_seeded_dir "${VAR_DIR}"; then printf '%s' "${VAR_DIR}" else printf 'missing; prep can create it' fi ;; log-dir) if check_seeded_dir "${LOG_DIR}"; then printf '%s' "${LOG_DIR}" else printf 'missing; prep can create it' fi ;; subtitles-base-dir) if check_seeded_dir "${SUBTITLES_BASE_DIR}"; then printf '%s' "${SUBTITLES_BASE_DIR}" else printf 'missing; prep can create it' fi ;; ffx-config) if check_seeded_file "${CONFIG_FILE}"; then printf '%s' "${CONFIG_FILE}" else printf 'missing; prep can seed a default non-destructively' fi ;; esac } report_toolchain_component() { local label="$1" local command_name="$2" local required="$3" if check_command_component "${label}" "${command_name}" "${required}"; then report_component ok "${label}" "$(component_detail "${command_name}")" else if [ "${required}" = "required" ]; then report_component failed "${label}" "$(component_detail "${command_name}")" MISSING_REQUIRED_SYSTEM+=("${command_name}") READINESS_FAILURES=$((READINESS_FAILURES + 1)) else report_component warn "${label}" "$(component_detail "${command_name}")" MISSING_OPTIONAL_SYSTEM+=("${command_name}") fi fi } report_tmdb_component() { if check_tmdb_key; then report_component ok "TMDB API key" "$(component_detail tmdb-key)" else report_component warn "TMDB API key" "$(component_detail tmdb-key)" fi } report_seeded_component() { local label="$1" local key="$2" local required="$3" local ok=1 case "${key}" in config-dir) check_seeded_dir "${CONFIG_DIR}" || ok=0 ;; var-dir) check_seeded_dir "${VAR_DIR}" || ok=0 ;; log-dir) check_seeded_dir "${LOG_DIR}" || ok=0 ;; subtitles-base-dir) check_seeded_dir "${SUBTITLES_BASE_DIR}" || ok=0 ;; ffx-config) check_seeded_file "${CONFIG_FILE}" || ok=0 ;; esac if [ "${ok}" -eq 1 ]; then report_component ok "${label}" "$(component_detail "${key}")" else if [ "${required}" = "required" ]; then report_component failed "${label}" "$(component_detail "${key}")" READINESS_FAILURES=$((READINESS_FAILURES + 1)) else report_component warn "${label}" "$(component_detail "${key}")" fi fi } print_dependency_status() { READINESS_FAILURES=0 MISSING_REQUIRED_SYSTEM=() MISSING_OPTIONAL_SYSTEM=() echo "Dependency status:" report_toolchain_component "git" "git" "required" report_toolchain_component "python3" "python3" "required" report_toolchain_component "ffmpeg" "ffmpeg" "required" report_toolchain_component "ffprobe" "ffprobe" "required" report_toolchain_component "cpulimit" "cpulimit" "required" report_tmdb_component } print_seeded_file_status() { echo "Seeded local files:" report_seeded_component "Config dir" "config-dir" "optional" report_seeded_component "Var dir" "var-dir" "optional" report_seeded_component "Log dir" "log-dir" "optional" report_seeded_component "Subtitles base dir" "subtitles-base-dir" "optional" report_seeded_component "ffx config" "ffx-config" "optional" } print_test_package_status() { if [ "${WITH_TESTS}" -eq 0 ]; then return 0 fi echo "Test environment notes:" report_component ok "system test dependencies" "no extra system packages beyond the standard runtime toolchain" report_component ok "Python test packages" "install via tools/setup.sh --with-tests" } detect_package_manager() { if command_exists apt-get; then printf 'apt-get\n' return 0 fi if command_exists pacman; then printf 'pacman\n' return 0 fi return 1 } run_root_command() { if [ "${EUID}" -eq 0 ]; then "$@" elif command_exists sudo; then sudo "$@" else return 1 fi } install_system_requirements() { local package_manager if ! package_manager="$(detect_package_manager)"; then printf 'No supported package manager found for automatic preparation.\n' >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi case "${package_manager}" in apt-get) printf 'Installing missing system dependencies via apt-get...\n' if ! run_root_command apt-get update; then printf 'apt-get update failed.\n' >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi if ! run_root_command apt-get install -y git python3 ffmpeg cpulimit; then printf 'apt-get install failed.\n' >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi ;; pacman) printf 'Installing missing system dependencies via pacman...\n' if ! run_root_command pacman -Sy --noconfirm git python ffmpeg cpulimit; then printf 'pacman install failed.\n' >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi ;; esac MUTATIONS=$((MUTATIONS + 1)) return 0 } render_default_config() { local output_path="$1" local temporary_output_path="" if [ ! -x "${FFX_PYTHON}" ]; then printf 'Missing bundle Python interpreter at %s.\n' "${FFX_PYTHON}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi if [ ! -f "${CONFIG_TEMPLATE_FILE}" ]; then printf 'Missing FFX config template at %s.\n' "${CONFIG_TEMPLATE_FILE}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi if ! temporary_output_path="$(mktemp "${output_path}.tmp.XXXXXX")"; then printf 'Failed to create a temporary config file next to %s.\n' "${output_path}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi if ! FFX_CONFIG_TEMPLATE_FILE="${CONFIG_TEMPLATE_FILE}" \ FFX_REPO_ROOT="${ROOT_DIR}" \ FFX_DATABASE_PATH="${DATABASE_FILE}" \ FFX_LOG_DIRECTORY="${LOG_DIR}" \ FFX_SUBTITLES_DIRECTORY="${SUBTITLES_BASE_DIR}" \ "${FFX_PYTHON}" - >"${temporary_output_path}" <<'PY' from __future__ import annotations import json import os import sys from pathlib import Path from jinja2 import Environment, FileSystemLoader, StrictUndefined repo_root = Path(os.environ["FFX_REPO_ROOT"]) src_root = repo_root / "src" if str(src_root) not in sys.path: sys.path.insert(0, str(src_root)) from ffx.constants import ( DEFAULT_SHOW_INDEX_EPISODE_DIGITS, DEFAULT_SHOW_INDEX_SEASON_DIGITS, DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS, DEFAULT_SHOW_INDICATOR_SEASON_DIGITS, ) template_path = Path(os.environ["FFX_CONFIG_TEMPLATE_FILE"]) environment = Environment( loader=FileSystemLoader(str(template_path.parent)), undefined=StrictUndefined, autoescape=False, keep_trailing_newline=True, ) template = environment.get_template(template_path.name) sys.stdout.write( template.render( database_path_json=json.dumps(os.environ["FFX_DATABASE_PATH"]), log_directory_json=json.dumps(os.environ["FFX_LOG_DIRECTORY"]), subtitles_directory_json=json.dumps(os.environ["FFX_SUBTITLES_DIRECTORY"]), default_index_season_digits=DEFAULT_SHOW_INDEX_SEASON_DIGITS, default_index_episode_digits=DEFAULT_SHOW_INDEX_EPISODE_DIGITS, default_indicator_season_digits=DEFAULT_SHOW_INDICATOR_SEASON_DIGITS, default_indicator_episode_digits=DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS, ) ) PY then rm -f "${temporary_output_path}" printf 'Failed to render ffx config from template %s.\n' "${CONFIG_TEMPLATE_FILE}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi if ! mv "${temporary_output_path}" "${output_path}"; then rm -f "${temporary_output_path}" printf 'Failed to move rendered ffx config into place at %s.\n' "${output_path}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi return 0 } seed_default_config() { if [ "${CHECK_ONLY}" -eq 1 ]; then return 0 fi local created_any=0 if [ ! -d "${CONFIG_DIR}" ]; then printf 'Creating config dir at %s...\n' "${CONFIG_DIR}" if ! mkdir -p "${CONFIG_DIR}"; then printf 'Failed to create config dir at %s.\n' "${CONFIG_DIR}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi created_any=1 fi if [ ! -d "${VAR_DIR}" ]; then printf 'Creating var dir at %s...\n' "${VAR_DIR}" if ! mkdir -p "${VAR_DIR}"; then printf 'Failed to create var dir at %s.\n' "${VAR_DIR}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi created_any=1 fi if [ ! -d "${LOG_DIR}" ]; then printf 'Creating log dir at %s...\n' "${LOG_DIR}" if ! mkdir -p "${LOG_DIR}"; then printf 'Failed to create log dir at %s.\n' "${LOG_DIR}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi created_any=1 fi if [ ! -d "${SUBTITLES_BASE_DIR}" ]; then printf 'Creating subtitles base dir at %s...\n' "${SUBTITLES_BASE_DIR}" if ! mkdir -p "${SUBTITLES_BASE_DIR}"; then printf 'Failed to create subtitles base dir at %s.\n' "${SUBTITLES_BASE_DIR}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi created_any=1 fi if [ ! -f "${CONFIG_FILE}" ]; then printf 'Seeding ffx config at %s...\n' "${CONFIG_FILE}" if ! render_default_config "${CONFIG_FILE}"; then return 1 fi created_any=1 fi if [ "${created_any}" -eq 1 ]; then MUTATIONS=$((MUTATIONS + 1)) fi return 0 } parse_args() { while [ "$#" -gt 0 ]; do case "$1" in --check) CHECK_ONLY=1 ;; --with-tests) WITH_TESTS=1 ;; --help|-h) usage exit 0 ;; *) printf 'Unknown option: %s\n\n' "$1" >&2 usage >&2 exit 2 ;; esac shift done } main() { parse_args "$@" print_dependency_status if [ "${CHECK_ONLY}" -eq 0 ] && [ "${#MISSING_REQUIRED_SYSTEM[@]}" -gt 0 ]; then install_system_requirements echo print_dependency_status fi echo print_seeded_file_status echo print_test_package_status if [ "${CHECK_ONLY}" -eq 0 ]; then seed_default_config echo print_dependency_status echo print_seeded_file_status echo print_test_package_status fi echo if [ "${INSTALL_FAILURES}" -gt 0 ]; then echo "One or more install steps failed; see the status checks above." >&2 return 1 fi if [ "${READINESS_FAILURES}" -gt 0 ]; then if [ "${CHECK_ONLY}" -eq 1 ]; then echo "Required system prerequisites are incomplete." >&2 else echo "Required components are still missing after preparation." >&2 fi return 1 fi if [ "${CHECK_ONLY}" -eq 1 ]; then echo "The FFX preparation environment is ready." elif [ "${MUTATIONS}" -gt 0 ]; then echo "The FFX preparation environment is ready." else echo "The FFX preparation environment is already prepared." fi return 0 } main "$@"