#!/bin/bash # assemblrr setup prompts — sourced by setup.sh # Provides: interactive prompt functions for service configuration # Requires: lib/core.sh (logging, read_masked, expand_path, directory helpers), # SETUP_MODE, SUPPORTED_MEDIA_SERVICES, APP_DEFAULT_INSTALL_DIR, # APP_DEFAULT_MEDIA_DIR, MEDIA_SUBDIRS (all set by setup.sh) set -euo pipefail _prompts_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=/dev/null [ -f "$_prompts_dir/ui.sh" ] && source "$_prompts_dir/ui.sh" configure_media_service() { # Express mode: use Jellyfin by default if [ "${SETUP_MODE:-}" = "express" ]; then media_service="jellyfin" media_service_port=8096 log_success "Media service: Jellyfin (port $media_service_port)" export media_service media_service_port return 0 fi ui_intro \ "Time to choose your media service." \ "Change the media server. This restarts the stack." echo " 1) Jellyfin (recommended, easier)" echo " 2) Emby" echo " 3) Plex (advanced, uses host networking)" while true; do read -p "Choose your media service [1]: " media_service_choice media_service_choice=${media_service_choice:-1} case "$media_service_choice" in 1) media_service="jellyfin"; break ;; 2) media_service="emby"; break ;; 3) media_service="plex"; break ;; *) media_service=$(echo "$media_service_choice" | awk '{print tolower($0)}') ;; esac if [[ ! " ${SUPPORTED_MEDIA_SERVICES[@]} " =~ " ${media_service} " ]]; then log_warning "\"$media_service\" is not supported. Please choose 1, 2, or 3." else break fi done # Set media service port if [ "$media_service" == "plex" ]; then media_service_port=32400 else media_service_port=8096 fi echo log_success "${APP_DISPLAY_NAME} will install \"$media_service\" on port \"$media_service_port\"" export media_service media_service_port } configure_quality_profile() { ui_intro \ "What default quality should movies and TV use (Radarr, Sonarr, and Seerr)?" \ "Change the default quality on Radarr, Sonarr, and Seerr." echo " 1) 4K (Ultra-HD — TV: assemblrr 4K WEB)" echo " 2) 1080p (TV: assemblrr 1080p WEB)" echo " 3) Any (stock *arr profile — no quality preference)" while true; do read -p "Choose your default quality profile [1]: " quality_choice quality_choice=${quality_choice:-1} case "$quality_choice" in 1) QUALITY_TIER="uhd"; break ;; 2) QUALITY_TIER="hd"; break ;; 3) QUALITY_TIER="any"; break ;; *) log_warning "Invalid choice. Please choose 1, 2, or 3." ;; esac done QUALITY_SOURCE="bluray" if [ "$QUALITY_TIER" != "any" ]; then echo echo " Movies: WEB only, or allow Blu-ray upgrades?" echo " (TV stays WEB. TRaSH has no English Blu-ray TV default.)" echo " 1) WEB only (smaller files; no Blu-ray upgrades)" echo " 2) Bluray + WEB (upgrades WEB to a Blu-ray encode when one exists)" while true; do read -p "Choose movie source [2]: " source_choice source_choice=${source_choice:-2} case "$source_choice" in 1) QUALITY_SOURCE="web"; break ;; 2) QUALITY_SOURCE="bluray"; break ;; *) log_warning "Invalid choice. Please choose 1 or 2." ;; esac done fi seerr_is_4k="false" [ "$QUALITY_TIER" = "uhd" ] && seerr_is_4k="true" export QUALITY_TIER QUALITY_SOURCE seerr_is_4k case "$QUALITY_TIER" in uhd) if [ "$QUALITY_SOURCE" = "web" ]; then log_success "Quality: 4K WEB for movies and TV" else log_success "Quality: 4K (movies: 4K Bluray + WEB, TV: 4K WEB)" fi ;; hd) if [ "$QUALITY_SOURCE" = "web" ]; then log_success "Quality: 1080p WEB for movies and TV" else log_success "Quality: 1080p (movies: 1080p Bluray + WEB, TV: 1080p WEB)" fi ;; *) log_success "Quality: Any" ;; esac } # Older name — setup / config edit used this when the question was Seerr-only. configure_seerr_profile() { configure_quality_profile } configure_subtitle_language() { # Jellyfin iso6392.txt: iso639-2/T|iso639-2/B|iso639-1|english_name|french_name local iso639_url="https://raw.githubusercontent.com/jellyfin/jellyfin/master/Emby.Server.Implementations/Localization/iso6392.txt" local awk_filter='$4 != "" {print $1 "\t" $4}' SELECTED_SUBTITLE_LANGUAGES=() FZF_SELECT_STATUS="cancelled" ui_intro \ "Select subtitle languages to download. If you pick more than one, a second menu asks which Jellyfin should prefer." \ "Change subtitle languages. If you pick more than one, a second menu asks which Jellyfin should prefer." if [ "${ASSEMBLRR_NONINTERACTIVE:-0}" = "1" ]; then log_info "Skipping subtitle language picker (non-interactive)." return 0 fi local data data=$(curl -sf --connect-timeout 5 "$iso639_url" 2>/dev/null | awk -F'|' "$awk_filter" | sort -k2) || true if [ -z "$data" ]; then log_info "Could not load language list — leaving subtitle language unchanged." return 0 fi local prompt header preselect_names="" preselect_names=$(_subtitle_stored_lang_codes) if ui_is_edit; then prompt="Subtitle languages> " header="Enter/TAB=toggle Ctrl-O=save Esc=cancel" else prompt="Subtitle languages> " header="Enter/TAB=toggle Ctrl-O=confirm Esc=skip" fi fzf_multi_select "$data" \ "$prompt" \ "$header" \ SELECTED_SUBTITLE_LANGUAGES \ "language" \ "$preselect_names" || true ui_picker_done "languages" if ui_picker_cancelled; then return 0 fi if [ "${#SELECTED_SUBTITLE_LANGUAGES[@]}" -eq 0 ]; then subtitle_language="" subtitle_languages="" log_success "Subtitle language: none (Bazarr defaults to English)" export subtitle_language subtitle_languages return 0 fi local preferred="${SELECTED_SUBTITLE_LANGUAGES[0]}" if [ "${#SELECTED_SUBTITLE_LANGUAGES[@]}" -gt 1 ]; then local current_pref="${SUBTITLE_LANGUAGE:-}" current_pref="${current_pref%%,*}" current_pref=${current_pref// /} local in_set=0 code for code in "${SELECTED_SUBTITLE_LANGUAGES[@]}"; do [ "$code" = "$current_pref" ] && in_set=1 && break done [ "$in_set" -eq 0 ] && current_pref="$preferred" local selected_data="" row for code in "${SELECTED_SUBTITLE_LANGUAGES[@]}"; do row=$(printf '%s\n' "$data" | awk -F'\t' -v c="$code" '$1 == c { print; exit }') [ -n "$row" ] && selected_data+="$row"$'\n' done ui_intro \ "Which of those should Jellyfin prefer for playback?" \ "Which of those should Jellyfin prefer for playback?" preferred=$(fzf_single_select_data \ "$selected_data" \ "Preferred language> " \ "↑↓=navigate Enter=confirm Esc=keep current preferred" \ "$current_pref") [ -z "$preferred" ] && preferred="$current_pref" in_set=0 for code in "${SELECTED_SUBTITLE_LANGUAGES[@]}"; do [ "$code" = "$preferred" ] && in_set=1 && break done [ "$in_set" -eq 0 ] && preferred="${SELECTED_SUBTITLE_LANGUAGES[0]}" fi subtitle_language="$preferred" subtitle_languages=$(_subtitle_join_preferred_first "$preferred" "${SELECTED_SUBTITLE_LANGUAGES[@]}") if [ "$subtitle_languages" = "$subtitle_language" ]; then log_success "Subtitle language: $subtitle_language (preferred)" else log_success "Subtitle languages: $subtitle_language (preferred), ${subtitle_languages#*,}" fi export subtitle_language subtitle_languages } # OpenSubtitles.com website login accepts email or username; the REST API (and # Bazarr) require the profile username. Consumer Api-Key is Bazarr's public key # from upstream so we validate the same path Bazarr will use at runtime. # Override with OPENSUBTITLES_API_KEY if needed. _verify_opensubtitles_login() { local user="$1" local pass="$2" local api_key="${OPENSUBTITLES_API_KEY:-s38zmzVlW7IlYruWi7mHwDYl2SfMQoC1}" local ua="${OPENSUBTITLES_USER_AGENT:-assemblrr/1.0}" local tmp http_code body local attempt tmp=$(mktemp) for attempt in 1 2 3; do http_code=$(curl -sS -o "$tmp" -w "%{http_code}" --connect-timeout 10 --max-time 30 \ -X POST "https://api.opensubtitles.com/api/v1/login" \ -H "Content-Type: application/json" \ -H "Api-Key: ${api_key}" \ -H "User-Agent: ${ua}" \ -d "$(jq -nc --arg u "$user" --arg p "$pass" '{username:$u,password:$p}')" \ 2>/dev/null || echo "000") body=$(cat "$tmp" 2>/dev/null || true) case "$http_code" in 200) if echo "$body" | jq -e '.token // empty' >/dev/null 2>&1; then rm -f "$tmp" return 0 fi rm -f "$tmp" return 1 ;; 429) log_warning "OpenSubtitles rate limit — retrying (${attempt}/3)..." sleep $((attempt + 1)) continue ;; 000) log_warning "OpenSubtitles: network error (could not reach API)" rm -f "$tmp" return 2 ;; *) # Prefer API message when present local msg msg=$(echo "$body" | jq -r '.message // empty' 2>/dev/null || true) if [ -n "$msg" ]; then log_warning "OpenSubtitles: $msg" else log_warning "OpenSubtitles: login failed (HTTP ${http_code})" fi rm -f "$tmp" return 1 ;; esac done rm -f "$tmp" return 1 } # Collect verified OpenSubtitles.com username/password into the usual globals. # $1 = cancel_on_empty_user (1 for config edit, 0 for setup). # Returns 0 on verified login, 1 on cancel/skip. _opensubtitles_ask_and_verify() { local cancel_on_empty="${1:-0}" local retry="" while true; do opensubtitles_username="" opensubtitles_password="" while [ -z "$opensubtitles_username" ]; do read_prompt "OpenSubtitles.com username (profile name, not email): " opensubtitles_username if [ -z "$opensubtitles_username" ]; then if [ "$cancel_on_empty" = "1" ]; then return 1 fi log_warning "Username cannot be empty." continue fi if [[ "$opensubtitles_username" == *@* ]]; then log_warning "That looks like an email. OpenSubtitles API rejects email logins." log_warning "Use your profile username (website login may accept email; Bazarr does not)." opensubtitles_username="" fi done while [ -z "$opensubtitles_password" ]; do if [ -r /dev/tty ]; then read_masked "OpenSubtitles.com password: " opensubtitles_password /dev/null || timedatectl show -p Timezone -v 2>/dev/null | tail -1 || echo "UTC") detected_tz=${detected_tz:-"UTC"} # Express mode: auto-detect timezone if [ "${SETUP_MODE:-}" = "express" ]; then tz="$detected_tz" log_success "Timezone: $tz (auto-detected)" export tz return 0 fi read -p "Timezone? [$detected_tz]: " tz tz=${tz:-$detected_tz} export tz } # running_services_location lives in lib/services.sh (sourced by setup via core path # or explicitly). Keep a thin wrapper if services.sh not loaded yet. if ! type running_services_location >/dev/null 2>&1; then running_services_location() { if [ -f "${_lib_dir:-}/services.sh" ]; then # shellcheck source=/dev/null source "${_lib_dir}/services.sh" running_services_location return fi echo "Service URLs: run ${APP_CLI_NAME:-assemblrr} status" } fi get_user_info() { # Express mode: use current user if [ "${SETUP_MODE:-}" = "express" ]; then username="$USER" puid=$(id -u "$username") pgid=$(id -g "$username") log_success "User: $username (PUID=$puid, PGID=$pgid)" export username puid pgid return 0 fi while true; do read -p "User to own the media server files? [$USER]: " username username=${username:-$USER} if id -u "$username" &>/dev/null; then puid=$(id -u "$username") pgid=$(id -g "$username") break else log_warning "User \"$username\" doesn't exist! Try again." fi done export username puid pgid } _probe_selected_storage() { local p root local -A seen=() for p in "$install_directory" "$media_directory"; do storage_needs_probe "$p" || continue root=$(storage_mount_root "$p") [ -n "$root" ] || continue [ -n "${seen[$root]:-}" ] && continue seen[$root]=1 log_info "Checking ${root} is responding..." if ! probe_writable_path "$root"; then echo log_info "Pick another location (Home is safest), or fix the drive and choose it again." return 1 fi done return 0 } # Setup is first-time only. A complete tree means use the CLI, not bootstrap again. refuse_setup_if_already_installed() { poke_wsl_automounts 2>/dev/null || true local existing="" existing=$(find_install_directory 2>/dev/null || true) if [ -z "$existing" ] || ! is_complete_assemblrr_install "$existing"; then return 0 fi INSTALL_DIR="$existing" load_install_pointer 2>/dev/null || true if [ -f "$existing/.assemblrr-config" ]; then safe_source "$existing/.assemblrr-config" elif [ -f "$existing/.${APP_NAME:-assemblrr}-config" ]; then safe_source "$existing/.${APP_NAME:-assemblrr}-config" fi MEDIA_DIRECTORY="${MEDIA_DIRECTORY:-}" local cli="${APP_CLI_NAME:-assemblrr}" echo log_warning "${APP_DISPLAY_NAME} is already installed." print_detected_locations echo log_info "Setup is first-time only. Use the CLI:" log_info " $cli upgrade # update the stack" log_info " $cli config edit # change settings" log_info " $cli uninstall # remove it, then run setup again" echo exit 1 } # Incomplete trees from an aborted setup. Offer to delete them before we start. offer_leftover_cleanup() { poke_wsl_automounts 2>/dev/null || true local leftover kind path ans leftover=$(list_assemblrr_leftovers || true) [ -n "$leftover" ] || return 0 local has_install=false local has_media=false if echo "$leftover" | grep -q $'^install\t'; then has_install=true fi if echo "$leftover" | grep -q $'^media\t'; then has_media=true fi echo log_warning "Found leftover ${APP_DISPLAY_NAME:-assemblrr} files from an earlier attempt:" while IFS=$'\t' read -r kind path; do [ -n "$path" ] || continue case "$kind" in install) log_warning " Install: $path" ;; media) log_warning " Media: $path" ;; *) log_warning " $path" ;; esac done <<< "$leftover" if [ "$has_install" = true ]; then local prompt_msg="Remove leftover install files? (Y/n) [Default = y]: " if [ "$has_media" = true ]; then prompt_msg="Remove leftover install files (keeps media)? (Y/n) [Default = y]: " fi read -p "$prompt_msg" ans ans=${ans:-y} if [ "${ans,,}" = "y" ]; then while IFS=$'\t' read -r kind path; do [ "$kind" = "install" ] && [ -n "$path" ] && safe_rm_rf "$path" done <<< "$leftover" fi fi leftover=$(list_assemblrr_leftovers || true) if echo "$leftover" | grep -q $'^media\t'; then local prompt_media="Remove leftover media directories? (y/N) [Default = n]: " if [ "$has_install" = true ]; then prompt_media="Also remove leftover media directories? (y/N) [Default = n]: " fi read -p "$prompt_media" ans ans=${ans:-n} if [ "${ans,,}" = "y" ]; then while IFS=$'\t' read -r kind path; do [ "$kind" = "media" ] && [ -n "$path" ] && safe_rm_rf "$path" done <<< "$leftover" fi fi } get_installation_paths() { ui_intro \ "Where should assemblrr store config and media? External drives (WSL E:, USB, …) show up in the list." \ "Change install and media directories. This does not move existing files." while true; do pick_storage_paths install_directory=$(expand_path "$install_directory") media_directory=$(expand_path "$media_directory") if ! _probe_selected_storage; then continue fi # Record both paths before any copy so uninstall can find an external # drive even if setup dies mid-copy. write_install_pointer "$install_directory" "$media_directory" # VPN gate bootstraps ~/assemblrr first. If the user then picked another # install path, drop that leftover tree so CLI discovery cannot shadow it. if [ -n "${_ASSEMBLRR_PROVISIONAL_INSTALL:-}" ] && \ [ "$install_directory" != "$_ASSEMBLRR_PROVISIONAL_INSTALL" ] && \ [ -d "$_ASSEMBLRR_PROVISIONAL_INSTALL" ]; then log_info "Install path changed — removing temporary files at $_ASSEMBLRR_PROVISIONAL_INSTALL" safe_rm_rf "$_ASSEMBLRR_PROVISIONAL_INSTALL" fi create_and_verify_directory "$install_directory" "installation" setup_directory_structure "$media_directory" verify_user_permissions "$username" "$media_directory" log_success "Install directory: $install_directory" log_success "Media directory: $media_directory" break done export install_directory media_directory }