Files
ffx/tools/configure_workstation.sh
2026-04-11 21:17:21 +02:00

495 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
set -u
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}"
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 <<EOF
Usage: $(basename "$0") [--check] [--with-tests] [--help]
Prepare the local workstation environment for an already installed FFX bundle.
Options:
--check Report readiness only. Do not create, install, or modify.
--with-tests Include test-related notes while preparing system dependencies and local config.
--help Show this help text.
Environment overrides:
FFX_CONFIG_DIR Override the parent directory for the seeded ffx.json file.
FFX_CONFIG_FILE Override the seeded config file path directly.
FFX_VAR_DIR Override the default data directory.
FFX_LOG_DIR Override the default log directory.
FFX_DATABASE_FILE Override the database path written into a newly seeded config.
FFX_SUBTITLES_BASE_DIR Override the default subtitles base directory written into a newly seeded config.
Notes:
- tools/setup.sh is the first installation step and owns bundle venv setup.
- This script is the second step and owns system dependencies plus local config.
- After the bundle is installed, the aligned CLI wrapper is: ffx configure_workstation
- Python test packages are installed by tools/setup.sh --with-tests, not here.
EOF
}
status_ok() {
printf '%sok%s' "${COLOR_GREEN}" "${COLOR_RESET}"
}
status_warn() {
printf '%swarn%s' "${COLOR_YELLOW}" "${COLOR_RESET}"
}
status_fail() {
printf '%sfailed%s' "${COLOR_RED}" "${COLOR_RESET}"
}
report_component() {
local level="$1"
local label="$2"
local detail="$3"
local rendered_status=""
case "${level}" in
ok)
rendered_status="$(status_ok)"
;;
warn)
rendered_status="$(status_warn)"
;;
*)
rendered_status="$(status_fail)"
;;
esac
printf '[%s] %s%s\n' "${rendered_status}" "${label}" "${detail:+: $detail}"
}
command_exists() {
command -v "$1" >/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
}
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 ! cat >"${CONFIG_FILE}" <<EOF
{
"databasePath": "${DATABASE_FILE}",
"logDirectory": "${LOG_DIR}",
"subtitlesDirectory": "${SUBTITLES_BASE_DIR}",
"metadata": {
"signature": {
"RECODED_WITH": "FFX"
},
"remove": [
"VERSION-eng",
"creation_time",
"NAME"
],
"streams": {
"remove": [
"BPS",
"NUMBER_OF_FRAMES",
"NUMBER_OF_BYTES",
"_STATISTICS_WRITING_APP",
"_STATISTICS_WRITING_DATE_UTC",
"_STATISTICS_TAGS",
"BPS-eng",
"DURATION-eng",
"NUMBER_OF_FRAMES-eng",
"NUMBER_OF_BYTES-eng",
"_STATISTICS_WRITING_APP-eng",
"_STATISTICS_WRITING_DATE_UTC-eng",
"_STATISTICS_TAGS-eng"
]
}
}
}
EOF
then
printf 'Failed to write ffx config at %s.\n' "${CONFIG_FILE}" >&2
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
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 "$@"