Skip to main content

IT - Programming - BASH scripting - MovieUpdateDraft.sh

#!/usr/bin/env bash
[[ "$(uname -a)" != *CYGWIN_NT* ]] && echo "This script can only be executed under Cygwin" && exit 1
# 🧠 ChatGPT-aware developer instructions at bottom of script (__CHATGPT_INSTRUCTIONS_BEGIN__)
################################################################################
# Script Name: MovieUpdateDraft.sh
# Description: Scans movie directories for files updated on a specific date,
#              grouped by year, and submits a DRAFT post to Blogger with a table of changes.
#
# Version History:
# v1.0  - 2025-04-16 - Initial version
# v1.1  - 2025-04-16 - Fixed Blogger API JSON parse error (HTML table incorrectly escaped)
# v1.2  - 2025-04-16 - Replaced literal '\n' with <br> in HTML content
# v1.3  - 2025-04-16 - Removed empty <li> element from bullet list
# v1.4  - 2025-04-16 - Added intro paragraph above the table describing update date
# v1.5  - 2025-04-16 - Formatted table header (blue background, white bold font)
# v1.6  - 2025-04-16 - Inserted HTML comment identifying generating script
# v1.7  - 2025-04-16 - Fixed invalid "REFERENCE_DATE" parsing bug (date CLI error)
# v1.8  - 2025-04-16 - Replaced "(updated REFERENCE_DATE)" with actual date text "(updated YYYY-MM-DD)"
# v1.9  - 2025-04-16 - Added support for --date and --debug switches
# v1.10 - 2025-04-16 - Adjusted verbosity logic for no-match and matched file output
# v1.11 - 2025-04-16 - Controlled "Checking title" debug echo using DEBUG flag
# v1.12 - 2025-04-16 - Sorted matched titles alphabetically within each year group
# v1.13 - 2025-04-16 - Added distinction between "added" vs. "changed" titles (based on presence of metadata.json)
# v1.14 - 2025-04-16 - Separated logic for new/changed titles into HTML output structure
# v1.15 - 2025-04-16 - Improved OMDb cache initialization and format validation
# v1.16 - 2025-04-16 - Implemented IMDb runtime and year consistency checks
# v1.17 - 2025-04-16 - Added collapsible file list support for each title row
# v1.18 - 2025-04-16 - Added support for multiple years using --year (cumulative)
# v1.19 - 2025-04-16 - Introduced optional HTML export toggle with --no-html
# v1.20 - 2025-04-16 - Appended update type (Added vs Changed) as suffix in bullet list
# v1.21 - 2025-04-16 - Finalized consistent year traversal and grouping logic
# v1.22 - 2025-04-16 - Optimised local OMDb metadata caching with minimal lookups

# Version History:
# v2.0  - 2025-04-16 - 🟦 Major release: integrated OMDb API, poster images, runtime, categories, collapsible UI, HTML export, Blogger DRAFT formatting, and comprehensive update type detection
# v2.1  - 2025-04-16 - Added --year (-y) and --range (-r) options to limit processing by year or year range
# v2.2  - 2025-04-16 - Changed YESTERDAY to REFERENCE_DATE throughout for clarity and accuracy
# v2.3  - 2025-04-16 - Added --publish flag to enable automatic scheduling as LIVE post
# v2.4  - 2025-04-16 - Added retry logic for transient curl DNS failures (curl error 6: Could not resolve host)
# v2.5  - 2025-04-16 - Fixed UTC scheduling issue by posting at 21:59 UTC to appear as 23:59 local
# v2.6  - 2025-04-16 - Added support for --title (-t) to override the default post title, and --label (-l) to add one or more Blogger labels
# v2.7  - 2025-04-16 - Added --force (-f) to skip confirmation warning when using --publish with filtered years
# v2.8  - 2025-04-16 - Token retrieval now retries indefinitely on failure with 2s backoff and visual feedback
# v2.9  - 2025-04-16 - Fixed logic bug in ScanDirectories(): OMDb was being called outside of loop context
# v2.10 - 2025-04-16 - Optimized OMDb API calls: lookup now occurs only for matched folders with updates
# v2.11 - 2025-04-16 - Appended OMDb genres in bold purple [Genre] to each title entry in the post (within <li>)
# v2.12 - 2025-04-16 - Added clickable IMDb links for each title using OMDb imdbID metadata
# v2.13 - 2025-04-16 - Appended runtime (e.g. 103 min) to genre tag for each title using OMDb metadata
# v2.14 - 2025-04-16 - Integrated TMDb actor lookup: new GetTopActorsFromTmdb() with name, role, and age (based on release year)
# v2.15 - 2025-04-16 - Introduced collapsible cast block using <details> + <summary> for cleaner blog display
# v2.16 - 2025-04-16 - Cast block supports up to 7 actors; age calculation based on release year minus birth year
# v2.17 - 2025-04-16 - Fixed bug: all Echo* output now routed via FD 10/11 to prevent HTML output contamination
# v2.18 - 2025-04-16 - TMDb key is now validated automatically at script start; --force-actors overrides failures
# v2.19 - 2025-04-16 - Blog post generation now correctly sorts titles alphabetically within each year
# v2.20 - 2025-04-16 - Fixed multibyte sort bug (e.g., emojis in <summary>) using LC_ALL=C
# v2.21 - 2025-04-19 - Refactored --check-deps to support default, local-only, and install modes
#
# Version History:
# v3.0  - 2025-04-19 - 🎭 Major release: implemented --actor mode for generating actor blog posts using TMDb/OMDb metadata
# v3.1  - 2025-04-19 - Added support for loading actor metadata from actor.db; fallback to TMDb if not cached
# v3.2  - 2025-04-20 - Implemented movie credits caching via movie_credits.db and chronological role sorting
# v3.3  - 2025-04-20 - Appended IMDb and TMDb movie links to filmography entries; fallback for missing codes as N/A
# v3.4  - 2025-04-21 - Introduced movie.db with support for title, release date, original title, and country
# v3.5  - 2025-04-21 - Added support for external link sections via links.db and biography via cached HTML
# v3.6  - 2025-04-22 - Added WATCHED_DATE field to movie.db; Watch column now displays ✅ icon if set
# v3.7  - 2025-04-22 - Introduced DBMovie_GetMovieWatchedDate; improved fallback logic for empty IMDb/TMDb codes
# v3.8  - 2025-04-22 - Standardized headers across all DBMovie_* and DoActor_* functions; updated SaveMovieMetadata to support optional watched date
# v3.9  - 2025-04-22 - Added MarkMovieAsWatched() helper function to update watch status in movie.db by TMDb ID
#
# Version History:
# v4.0  - 2025-04-23 - 🎬 Extended movie.db format with BLOGGER_URL and PHOTO_URL fields; DBMovie_SaveMovieMetadata updated accordingly
#==============================================================================

# To do:
#   - at exit, remove all possible temp files
#
# Now, an update for the --actor mode
##### --show-poster-url: similar to blogger-url but this time for the actor photo-url
##### --delete-poster-url: similar to blogger-url but this time for the actor photo-url
##### --update-poster-url <url>: similar to blogger-url but this time for the actor photo-url
##### --list-poster-url [<n>]: similar to blogger-url but this time for the actor photo-url
##### --revert-poster-url <n>: similar to blogger-url but this time for the actor photo-url
#
# William Powell: 
# <div class="separator" style="clear: both; text-align: center;">
#   <a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEia7QyVwhtKpc7G1hBO8W9PGz8v8Mwp3OCEfnVhmFLlKGzKOxY-uKMzTBaPy8ERaz678x0PY_UajAN1gNTPqTTSNTM7bz75tAYAXKSmPkHzwOpHd5KUHfMPrvTXPtLdxhUugus74Vxm5aEIiYCEExhF1zHUm5R4AB3vOL1SfisKDjFeiGuJbbdtOkX3zD0K/s1256/MV5BMTk4MzcxMjExN15BMl5BanBnXkFtZTcwODA2NDQyOA@@._V1_.jpg" style="margin-left: 1em; margin-right: 1em;">
#     <img border="0" 
#          data-original-height="1256" 
#          data-original-width="1000" 
#          height="640" 
#          src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEia7QyVwhtKpc7G1hBO8W9PGz8v8Mwp3OCEfnVhmFLlKGzKOxY-uKMzTBaPy8ERaz678x0PY_UajAN1gNTPqTTSNTM7bz75tAYAXKSmPkHzwOpHd5KUHfMPrvTXPtLdxhUugus74Vxm5aEIiYCEExhF1zHUm5R4AB3vOL1SfisKDjFeiGuJbbdtOkX3zD0K/w510-h640/MV5BMTk4MzcxMjExN15BMl5BanBnXkFtZTcwODA2NDQyOA@@._V1_.jpg" 
#          width="510" />
#   </a>
# </div>
#
# Now that the script is working again, let's focus on --movie. 
## --movie will require at least one of below.
####  --name <movie title> [--year <release-year]
####  --imdb <idmb-movie-id>
####  --tmdb <tmdb-movie-id>
## If there is no record in movie.db for the mvoie, download all the movieinfromation (exatcly as the same in --actor).
## Load all the cast for the mvoie and create entries in movie_credits.db. For each new cast member, create an entry in actor.db 
## if --verbose is used, show messages for each new movie title created and each actor created
## if --force-update is used, even if the inforamtion for the movie and/or actor is present, gather it again from TMDB and/or IMDB and record any changes.

## after that the user has to chose the execution sub mode (as in --actor) [all the modes are mutually exclusive]
#### --blog -> IF the blogger-url field is empty generates an new entry in the blogger for this movie (similar to what happens with --actor)
#### --blog -> if the blogger-url field is not-empty, present the menu with the oprions for
######## a) /dev/clipboard. 
######## b) output fo file <movie-id>.htlm
######## c) create a new blogger entry and replace the current movie.db blogger-url
######## d) update the existing blogger post (not activate at this stage)
######## If the user pass --force-clipboard, assume option (a)
######## if the user pass --force-file, assume option (b)
######## if the user pass --force-new, assume (c)
######## if the user pass --force-update, assume (d)
######## all those --force options are mutually exclusive
#
#### --show-blogger-url: show the current blogger url for the movie
#### --delete-blogger-url: clean/erase/dele/remove/nullify the current blogger url (preserve eveyrhting else for the movie record). record in log.
#### --update-blogger-url <url>: change the blogger url for a new url. record in log
#### --list-blogger-url [<n>]: list the previous <n> blogger url used (n defauult to 10)
#### --revert-blogger-url [<n>]: revert blogger url to previous state (n default to 1)
#
#### --show-poster-url: similar to blogger-url but this time for the movie photo-url
#### --delete-poster-url: similar to blogger-url but this time for the movie photo-url
#### --update-poster-url <url>: similar to blogger-url but this time for the movie photo-url
#### --list-poster-url [<n>]: similar to blogger-url but this time for the movie photo-url
#### --revert-poster-url <n>: similar to blogger-url but this time for the movie photo-url
#
#### --show-wacth: similar to blogger-url but this time for the the watch date
#### --delete-wacth: similar to blogger-url but this time for the watch date
#### --update-wacth <yyyy-mm-dd>: similar to blogger-url but this time for the watch date
#### --list-wacth [<n>]: similar to blogger-url but this time for the watch date
#### --revert-wacth <n>: similar to blogger-url but this time for the watch date

################################################################################

#grep -E '^\s*(function\s+)?[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)\s*\{' MovieUpdateDraft.sh | sed -E 's/^\s*(function\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\).*/\2/' | sort

# Load shared library
[[ ! -e ~/MY_LIBRARY.sh ]] && { echo "***ERROR: Cannot find library" ; exit 1; }
[[ ! -r ~/MY_LIBRARY.sh ]] && { echo "***ERROR: Library is not readable" ; exit 1; }
source ~/MY_LIBRARY.sh || { echo "***ERROR: Cannot source library" ; exit 1; }

[[ ! -e ~/MY_SECRETS.sh ]] && { echo "***ERROR: Cannot find ~/MY_SECRETS.sh" ; exit 1; }
[[ ! -r ~/MY_SECRETS.sh ]] && { echo "***ERROR: ~/MY_SECRETS.sh is not readable" ; exit 1; }
source ~/MY_SECRETS.sh || { echo "***ERROR: Cannot source ~/MY_SECRETS.sh" ; exit 1; }

set -o monitor
trap 'echo;Echo "💥 Caught interrupt signal, exiting."; exit 130' SIGINT SIGTERM

# Constants
readonly SCRIPT_VERSION="3.9"
readonly MOVIE_ROOT="/cygdrive/f/MediaLibrary/Movies/!By_Year"

# Variables
declare -A year_map=()
declare -a FILTER_YEARS=()
declare -a LABELS=()
declare -i VERBOSE=0
declare -i VERBOSE_DEPS=0
declare -i DRYRUN=0
declare -i DEBUG=0
declare -a POSITIONAL_ARGS=()
declare -i TMDB_KEY_VALID=0
declare -i CHECK_DEPS_DEFAULT=0
declare -i CHECK_DEPS_LOCAL=1
declare -i CHECK_DEPS_INSTALL=0
declare -i DRY_RUN=0

declare -i EXECUTION_MODE=0  # 0 = undefined, 1 = movie list, 2 = actor mode, etc.
declare -i EXECUTION_SUBMODE=0

#----- Movie List Variables
declare CUSTOM_DATE=""
declare -i USE_HTML=1
declare -i FORCE_PUBLISH=0
declare CUSTOM_TITLE=""
declare -i FORCE=0
declare -i FORCE_ACTORS=0

#----- ACTOR mode variable
declare ACTOR_QUERY=""
declare -i UPDATE_MODE=0
declare ACTOR_UPDATE_PATH=""
declare IMDB_ID=""
declare ACTOR_QUERY=""
declare ACTOR_IMDB_ID=""
declare NEW_BLOGGER_URL=
declare NEW_PHOTO_URL=
declare -gi FORCE_BIO=0
declare _ACTOR_SEARCH_RESULT_JSON=

# Metadata Cache
declare -A METADATA_CACHE=()
readonly METADATA_FILE_NAME="metadata.json"
readonly OMDB_CACHE_FILE="/tmp/.omdb_cache.json"

#******************************************************************************
#****************** Local Movie Database cache functions **********************
#******************************************************************************
#**************** This code should be move to MY_DB.sh later. It wil be sourced 

#-----------------------------------------------------------------------------#
#                                                                             |
#                                                                             |
#               C O N S T A N T S   &   V A R I A B L E S                     |
#                                                                             |
#                                                                             |
#-----------------------------------------------------------------------------#

readonly MY_MDB_CACHE="${HOME}/.MyMDBCache"
readonly ACTOR_DB_PATH="${MY_MDB_CACHE}/actor.db"
readonly MOVIE_DB_PATH="${MY_MDB_CACHE}/movie.db"
readonly MOVIE_CREDITS_DB_PATH="${MY_MDB_CACHE}/movie_credits.db"
readonly LINKS_DB_PATH="${MY_MDB_CACHE}/links.db"

readonly MANY_RECORD_MARK="***MANY***"

declare ACTOR_RECORD_LOADED=
declare ACTOR_TMDB_ID=
declare ACTOR_IMDB_ID=
declare ACTOR_NAME=
declare ACTOR_DOB=
declare ACTOR_DOD=
declare ACTOR_PHOTO_URL=
declare ACTOR_GENDER=
declare ACTOR_DEPTO=
declare ACTOR_BLOG_URL=
declare ACTOR_BIRTH_YEAR=

declare MOVIE_RECORD_LOADED=
declare MOVIE_TMDB_ID
declare MOVIE_IMDB_ID
declare MOVIE_COUNTRY
declare MOVIE_TITLE_EN 
declare MOVIE_TITLE_ORIG 
declare MOVIE_RELEASE_DATE 
declare MOVIE_EXISTING_WATCHED_DATE 
declare MOVIE_BLOGGER_URL 
declare MOVIE_PHOTO_URL

#-----------------------------------------------------------------------------#
#                                                                             |
#                                                                             |
#                                A C T O R                                    |
#                                                                             |
#                                                                             |
#-----------------------------------------------------------------------------#

#==============================================================================
# DBMovie_InitActorDb: Initialize actor.db if missing or empty
#------------------------------------------------------------------------------
# Description:
#   Ensures that the actor metadata cache file (`actor.db`) exists and contains
#   the appropriate header. If the file does not exist or is empty, it will be
#   created with the following pipe-separated column headers:
#
#     TMDB_ID|IMDB_ID|NAME|DOB|DOD|GENDER|DEPARTMENT|PHOTO_URL|BLOGGER_URL
#
# Globals Used:
#   ACTOR_DB_PATH - Full path to the actor.db file
#   DEBUG         - If set, logs the creation of the file
#
# Outputs:
#   - Creates actor.db with header if the file is missing or empty
#   - Logs a debug message if DEBUG is enabled
#
# Return:
#   0 - Success
#   1 - Failure (unable to create file)
#==============================================================================
DBMovie_InitActorDb() {
  local -r file="${ACTOR_DB_PATH}"

  if [[ ! -s "${file}" ]]; then
    mkdir -p "$(dirname "${file}")" 2>/dev/null
    if [[ ! -s "${file}" ]]; then
      echo "TMDB_ID|IMDB_ID|NAME|DOB|DOD|GENDER|DEPARTMENT|PHOTO_URL|BLOGGER_URL" > "${file}" || {
        EchoE "${FUNCNAME[0]}:${LINENO}: Failed to create ${file}."
        return 1
      }
      (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Created ${file} with new header."
    fi
  fi

  return 0
}

#==============================================================================
# ResolveActorTmdbIdFromImdb
#
# Given an IMDb ID, find and return the corresponding TMDb ID from actor.db.
#
# Arguments:
#   ${1} - IMDb ID (e.g., nm0000493)
#
# Returns:
#   Echoes the TMDb ID to stdout
#   0 - if found
#   1 - if not found
#==============================================================================
ResolveActorTmdbIdFromImdb() {
  local -r imdb_id="${1}"
  local -r file="${ACTOR_DB_PATH}"

  [[ -z "${imdb_id}" ]] && return 1
  DBMovie_InitActorDb || return 1

  awk -F'|' -v id="${imdb_id}" '$2 == id { print $1; exit }' "${file}" | grep -q . && return 0 || return 1
}

#==============================================================================
# ResolveActorImdbIdFromTmdb
#
# Given a TMDb ID, find and return the corresponding IMDb ID from actor.db.
#
# Arguments:
#   ${1} - TMDb ID (e.g., 32428)
#
# Returns:
#   Echoes the IMDb ID to stdout
#   0 - if found
#   1 - if not found
#==============================================================================
ResolveActorImdbIdFromTmdb() {
  local -r tmdb_id="${1}"
  local -r file="${ACTOR_DB_PATH}"

  [[ -z "${tmdb_id}" ]] && return 1
  DBMovie_InitActorDb || return 1

  awk -F'|' -v id="${tmdb_id}" '$1 == id { print $2; exit }' "${file}" | grep -q . && return 0 || return 1
}

#==============================================================================
# ResolveActorIds
#
# Ensures both IMDb and TMDb IDs are known for a given actor, first checking
# local cache (actor.db) before querying TMDb API. Updates local cache with
# full available metadata if missing.
#
# Arguments:
#   ${1} - TMDb ID (e.g., 32428) or empty
#   ${2} - IMDb ID (e.g., nm0000493) or empty
#
# Globals modified:
#   ACTOR_TMDB_ID
#   ACTOR_IMDB_ID
#
# Outputs:
#   - Echoes the resolved TMDb ID to stdout
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
ResolveActorIds() {
  local tmdb_id_arg=""
  local imdb_id_arg=""
  local record=""
  local -r FUNC="${FUNCNAME[0]}"

  # Handle single-parameter usage (auto-detect type)
  if [[ -n "${1}" && -z "${2}" ]]; then
    if [[ "${1}" =~ ^nm[0-9]+$ ]]; then
      imdb_id_arg="${1}"
    elif [[ "${1}" =~ ^[0-9]+$ ]]; then
      tmdb_id_arg="${1}"
    else
      EchoE "${FUNC}:${LINENO}: Invalid actor ID format: '${1}'"
      return 1
    fi
  else
    tmdb_id_arg="${1:-}"
    imdb_id_arg="${2:-}"
  fi
  readonly tmdb_id_arg
  readonly imdb_id_arg
  
  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: TMDb ID=${tmdb_id_arg}, IMDb ID=${imdb_id_arg}"
  
  #-----------------------------
  # Step 1: Try cache via TMDb ID
  #-----------------------------
  if [[ -n "${tmdb_id_arg}" ]]; then
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Attempting cache lookup by TMDb ID..."
    if record=$(DBMovie_LoadActorRecord "${tmdb_id_arg}"); then
      IFS='|' read -r actor_tmdb actor_imdb _ <<< "${record}"
      if [[ -n "${actor_imdb}" ]]; then
        export ACTOR_TMDB_ID="${actor_tmdb}"
        export ACTOR_IMDB_ID="${actor_imdb}"
        (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found in cache: TMDb=${ACTOR_TMDB_ID}, IMDb=${ACTOR_IMDB_ID}"
        return 0
      else
        EchoD "${FUNC}:${LINENO}: IMDb ID missing in local cache, need online resolution."
      fi
    fi
  fi

  #-----------------------------
  # Step 2: Try cache via IMDb ID
  #-----------------------------
  if [[ -n "${imdb_id_arg}" ]]; then
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Attempting cache lookup by IMDb ID..."
    if record=$(DBMovie_LoadActorRecord "${imdb_id_arg}"); then
      IFS='|' read -r actor_tmdb actor_imdb _ <<< "${record}"
      if [[ -n "${actor_tmdb}" ]]; then
        export ACTOR_TMDB_ID="${actor_tmdb}"
        export ACTOR_IMDB_ID="${actor_imdb}"
        (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found in cache: TMDb=${ACTOR_TMDB_ID}, IMDb=${ACTOR_IMDB_ID}"
        return 0
      fi
    fi
  fi

  #-----------------------------
  # Step 3: Online resolution
  #-----------------------------
  local response=""
  local tmdb_id=""
  local imdb_id=""
  local name=""
  local dob=""
  local dod=""
  local photo_url=""
  local gender=""
  local department=""

  if [[ -n "${imdb_id_arg}" ]]; then
    EchoW "${FUNC}:${LINENO}: IMDb ID '${imdb_id_arg}' not found locally, querying TMDb..."
    response=$(curl --silent --fail \
      "https://api.themoviedb.org/3/find/${imdb_id_arg}?api_key=${TMDB_API_KEY}&external_source=imdb_id") || {
      EchoE "${FUNC}:${LINENO}: Failed to resolve via IMDb ID ${imdb_id_arg}."
      return 1
    }

    tmdb_id=$(jq -r '.person_results[0].id // empty' <<< "${response}")
    name=$(jq -r '.person_results[0].name // empty' <<< "${response}")
    dob=$(jq -r '.person_results[0].birthday // empty' <<< "${response}")
    dod=$(jq -r '.person_results[0].deathday // empty' <<< "${response}")
    photo_url=$(jq -r '.person_results[0].profile_path // empty' <<< "${response}")
    gender=$(jq -r '.person_results[0].gender // empty' <<< "${response}")
    department=$(jq -r '.person_results[0].known_for_department // empty' <<< "${response}")

    [[ -n "${photo_url}" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"

    if [[ -z "${tmdb_id}" ]]; then
      EchoE "${FUNC}:${LINENO}: No TMDb ID found for IMDb ID ${imdb_id_arg}."
      return 1
    fi

    DBMovie_SaveActorRecord "${tmdb_id}" "${imdb_id_arg}" "${name}" "${dob}" "${dod}" "${gender}" "${department}" "${photo_url}" "" 

    export ACTOR_TMDB_ID="${tmdb_id}"
    export ACTOR_IMDB_ID="${imdb_id_arg}"
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Resolved and cached: TMDb=${tmdb_id}, IMDb=${imdb_id_arg}"
    return 0
  fi

  if [[ -n "${tmdb_id_arg}" ]]; then
    EchoW "${FUNC}:${LINENO}: TMDb ID '${tmdb_id_arg}' not found locally, querying TMDb..."
    response=$(curl --silent --fail \
      "https://api.themoviedb.org/3/person/${tmdb_id_arg}?api_key=${TMDB_API_KEY}") || {
      EchoE "${FUNC}:${LINENO}: Failed to resolve via TMDb ID ${tmdb_id_arg}."
      return 1
    }

    imdb_id=$(jq -r '.imdb_id // empty' <<< "${response}")
    name=$(jq -r '.name // empty' <<< "${response}")
    dob=$(jq -r '.birthday // empty' <<< "${response}")
    dod=$(jq -r '.deathday // empty' <<< "${response}")
    photo_url=$(jq -r '.profile_path // empty' <<< "${response}")
    gender=$(jq -r '.gender // empty' <<< "${response}")
    department=$(jq -r '.known_for_department // empty' <<< "${response}")

    [[ -n "${photo_url}" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"

    if [[ -z "${imdb_id}" ]]; then
      EchoE "${FUNC}:${LINENO}: No IMDb ID found for TMDb ID ${tmdb_id_arg}."
      return 1
    fi

    DBMovie_SaveActorRecord "${tmdb_id_arg}" "${imdb_id}" "${name}" "${dob}" "${dod}" "${gender}" "${department}" "${photo_url}" ""

    export ACTOR_TMDB_ID="${tmdb_id_arg}"
    export ACTOR_IMDB_ID="${imdb_id}"
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Resolved and cached: TMDb=${tmdb_id_arg}, IMDb=${imdb_id}"
    return 0
  fi

  EchoE "${FUNC}:${LINENO}: No valid TMDb or IMDb ID provided."
  return 1
}

#==============================================================================
# DBMovie_SaveActorRecord
#
# Save or update a record in actor.db using TMDb ID as the primary key.
# If TMDb ID is missing, falls back to IMDb ID. Sanitizes all fields.
#
# Globals used:
#   - ACTOR_DB_PATH
#
# Arguments:
#   ${1} - TMDb ID (e.g., 123456)
#   ${2} - IMDb ID (e.g., nm0000123)
#   ${3} - Name
#   ${4} - Date of birth (YYYY-MM-DD)
#   ${5} - Date of death (YYYY-MM-DD)
#   ${6} - Gender (Male, Female, Unknown)
#   ${7} - Department (e.g., Acting, Directing)
#   ${8} - Photo URL
#   ${9} - Blogger post URL
#
# Outputs:
#   - Updates actor.db safely
#   - Logs debug info via EchoD
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_SaveActorRecord() {
  local -r tmdb_id="${1}"
  local -r imdb_id="${2}"
  local name="${3}"
  local -r dob="${4}"
  local -r dod="${5}"
  local gender="${6}"
  local department="${7}"
  local photo_url="${8}"
  local blog_url="${9}"
  local -r file="${ACTOR_DB_PATH}"

  DBMovie_InitActorDb || return 1

  local -r safe_tmdb_id="$(SanitizeField "${tmdb_id}")"
  local -r safe_imdb_id="$(SanitizeField "${imdb_id}")"
  name="$(SanitizeField "${name}")"
  local -r safe_dob="$(SanitizeField "${dob}")"
  local -r safe_dod="$(SanitizeField "${dod}")"
  gender="$(SanitizeField "${gender}")"
  department="$(SanitizeField "${department}")"
  local -r safe_photo_url="$(SanitizeField "${photo_url}")"
  local -r safe_blog_url="$(SanitizeField "${blog_url}")"

  # Validate minimal requirements
  if [[ -z "${safe_tmdb_id}" && -z "${safe_imdb_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Cannot save actor record — both TMDb and IMDb IDs are missing."
    return 1
  fi
  if [[ -n "${safe_tmdb_id}" && ! "${safe_tmdb_id}" =~ ^[0-9]+$ ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Invalid TMDb ID format '${safe_tmdb_id}'. Must be numeric."
    return 1
  fi
  if [[ -n "${safe_imdb_id}" && ! "${safe_imdb_id}" =~ ^nm[0-9]+$ ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Invalid IMDb ID format '${safe_imdb_id}'. Must start with 'nm'."
    return 1
  fi

  # Ensure database file exists
  DBMovie_InitActorDb || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to initialize actor.db"
    return 1
  }

  # Remove any existing record by TMDb ID or IMDb ID
  grep -v -E "^(${safe_tmdb_id}\||${safe_imdb_id}\|)" "${file}" > "${file}.tmp" 2>/dev/null || true

  # Append the sanitized new record
  echo "${safe_tmdb_id}|${safe_imdb_id}|${name}|${safe_dob}|${safe_dod}|${gender}|${department}|${safe_photo_url}|${safe_blog_url}" >> "${file}.tmp"
  EchoD "${FUNCNAME[0]}:${LINENO}: record='${safe_tmdb_id}|${safe_imdb_id}|${name}|${safe_dob}|${safe_dod}|${gender}|${department}|${safe_photo_url}|${safe_blog_url}'"

  # Replace the database
  mv "${file}.tmp" "${file}" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to update actor.db"
    return 1
  }

  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Saved actor: TMDb=${safe_tmdb_id}, IMDb=${safe_imdb_id}, Name='${name}'"
  return 0
}

#==============================================================================
# DBMovie_LoadActorRecordOnline
#
# Fetch an actor's metadata online using TMDb API and optionally IMDb,
# and create a new local record in actor.db.
#
# Globals used:
#   - TMDB_API_KEY
#   - ACTOR_DB_PATH
#
# Globals modified:
#   - actor.db (new record inserted)
#
# Arguments:
#   ${1} - Actor ID (TMDb ID or IMDb ID)
#
# Return:
#   0 - Success (record created)
#   1 - Failure (record not fetched)
#==============================================================================
DBMovie_LoadActorRecordOnline() {
  EchoE "${FUNCNAME[0]}:${LINENO}: 1=${1}"
  local -r id="${1}"
  local tmdb_id=""
  local imdb_id=""

  DBMovie_InitActorDb || return 1
  
  # Resolve whether ID is IMDb or TMDb
  if [[ "${id}" =~ ^nm[0-9]+$ ]]; then
    # IMDb ID — resolve to TMDb
    tmdb_id="$(ResolveActorTmdbIdFromImdb "${id}")" || return 1
    imdb_id="${id}"
  else
    # Assume TMDb ID
    tmdb_id="${id}"
    imdb_id="$(ResolveActorImdbIdFromTmdb "${id}")" || return 1
  fi

  EchoE "${FUNCNAME[0]}:${LINENO}: tmdb_id=${tmdb_id} imdb_id=${imdb_id}"


  # Fetch full actor details
  local json
  json="$(curl --silent --fail "https://api.themoviedb.org/3/person/${tmdb_id}?api_key=${TMDB_API_KEY}")" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch actor metadata from TMDb."
    return 1
  }

  # Parse fields
  local name dob dod gender_code department photo_url gender_text=""
  name="$(jq -r '.name // empty' <<< "${json}")"
  dob="$(jq -r '.birthday // empty' <<< "${json}")"
  dod="$(jq -r '.deathday // empty' <<< "${json}")"
  gender_code="$(jq -r '.gender // empty' <<< "${json}")"
  department="$(jq -r '.known_for_department // empty' <<< "${json}")"
  photo_url="$(jq -r '.profile_path // empty' <<< "${json}")"

  [[ -n "${photo_url}" && "${photo_url}" != "null" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"

  if [[ -z "${name}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Actor metadata incomplete, cannot create record."
    return 1
  fi

  # Save to cache
  DBMovie_SaveActorRecord "${tmdb_id}" "${imdb_id}" "${name}" "${dob}" "${dod}" "${gender_code}" "${department}" "${photo_url}" "" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to save actor record."
    return 1
  }

  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Saved actor '${name}' (TMDb ID=${tmdb_id}) to cache."
  return 0
}

#==============================================================================
# DBMovie_LoadActorRecord
#
# Load an actor's full record from actor.db. If not found, fetch online
# and update the cache automatically.
#
# Globals Used:
#   ACTOR_DB_PATH
#   ACTOR_RECORD_LOADED
#
# Globals Modified:
#   ACTOR_RECORD_LOADED
#
# Arguments:
#   ${1} - Actor ID (TMDb ID or IMDb ID)
#
# Return:
#   0 - Success (record loaded)
#   1 - Failure
#==============================================================================
DBMovie_LoadActorRecord() {
  local -r id="${1}"
  local -r file="${ACTOR_DB_PATH}"
  local -r FUNC="${FUNCNAME[0]}"

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: 1=${1}"

  if [[ -z "${id}" ]]; then
    EchoE "${FUNC}:${LINENO}: No ID provided."
    return 1
  fi

  DBMovie_InitActorDb || return 1
  
  local record=""
  local -i attempt=0

  while (( attempt < 2 )); do
    if [[ "${id}" =~ ^nm[0-9]+$ ]]; then
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}: IMDB id=${id}"
      # IMDb ID lookup (field 2)
      record="$(awk -F'|' -v key="${id}" 'NR>1 && $2==key {print; exit}' "${file}")"
    else
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}: TMDB id=${id}"
      # TMDb ID lookup (field 1)
      record="$(awk -F'|' -v key="${id}" 'NR>1 && $1==key {print; exit}' "${file}")"
    fi

    if [[ -n "${record}" ]]; then
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}: record=${record}"
      export ACTOR_RECORD_LOADED="${record}"
      echo "${record}"
      return 0
    fi

    if (( attempt == 0 )); then
      EchoW "${FUNC}:${LINENO}: Actor ID '${id}' not found locally. Attempting online retrieval..."
      DBMovie_LoadActorRecordOnline "${id}" >/dev/null || {
        EchoE "${FUNC}:${LINENO}: Failed to retrieve actor ID '${id}' online."
        return 1
      }
    fi

    (( attempt++ ))
  done

  EchoE "${FUNC}:${LINENO}: Actor ID '${id}' still not found after online retrieval."
  return 1
}

#==============================================================================
# DBMovie_LoadActorRecordParse
#
# Fetch actor metadata from TMDb and IMDb and store it in the local cache.
#
# Arguments:
#   $1 - TMDb ID (e.g., "12345") or empty
#   $2 - IMDb ID (e.g., "nm0000001") or empty
#
# Globals Used:
#   - TMDB_API_KEY
#   - MY_MDB_CACHE
#
# Globals Modified:
#   - ACTOR_TMDB_ID
#   - ACTOR_IMDB_ID
#   - ACTOR_NAME
#   - ACTOR_DOB
#   - ACTOR_DOD
#   - ACTOR_GENDER
#   - ACTOR_DEPTO
#   - ACTOR_PHOTO_URL
#   - ACTOR_BLOG_URL
#   - ACTOR_BIRTH_YEAR
#
# Returns:
#   0 on success, 1 on failure
#==============================================================================
DBMovie_LoadActorRecordParse() {
  local -r tmdb_id_arg="${1:-}"
  local -r imdb_id_arg="${2:-}"
  local -r FUNC="${FUNCNAME[0]}"

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: tmdb_id=${1} imdb_id=${2}"

  DBMovie_InitActorDb || return 1
  ResolveActorIds "${tmdb_id_arg}" "${imdb_id_arg}" || return 1

  local -r tmdb_id="${ACTOR_TMDB_ID}"
  local -r imdb_id="${ACTOR_IMDB_ID}"

  local record=""
  if record="$(DBMovie_LoadActorRecord "${tmdb_id}")"; then
    local rec_tmdb_id rec_imdb_id rec_name rec_dob rec_dod rec_gender rec_depto rec_photo rec_blog
    IFS='|' read -r rec_tmdb_id rec_imdb_id rec_name rec_dob rec_dod rec_gender rec_depto rec_photo rec_blog <<< "${record}"

    export ACTOR_TMDB_ID="${rec_tmdb_id}"
    export ACTOR_IMDB_ID="${rec_imdb_id}"
    export ACTOR_NAME="${rec_name}"
    export ACTOR_DOB="${rec_dob}"
    export ACTOR_DOD="${rec_dod}"
    export ACTOR_GENDER="${rec_gender}"
    export ACTOR_DEPTO="${rec_depto}"
    export ACTOR_PHOTO_URL="${rec_photo}"
    export ACTOR_BLOG_URL="${rec_blog}"
    export ACTOR_BIRTH_YEAR="${rec_dob:0:4}"

    Echo "📇 Loaded actor details from cache: ${ACTOR_NAME} (${ACTOR_DOB:-N/A}..${ACTOR_DOD:-N/A}) [TMDb: ${ACTOR_TMDB_ID}, IMDb: ${ACTOR_IMDB_ID}]"
    return 0
  else
    EchoE "${FUNC}:${LINENO}: Cannot read actor record from cache [TMDb=${tmdb_id}, IMDb=${imdb_id}]"
    return 1
  fi
}

#==============================================================================
# DBMovie_UpdateActorBloggerUrl
#
# Update the Blogger URL field for an existing actor record.
#
# Globals used:
#   - ACTOR_DB_PATH
#
# Globals modified:
#   - actor.db (one record updated)
#
# Arguments:
#   ${1} - TMDb ID or IMDb ID of the actor
#   ${2} - New Blogger URL (unsanitized)
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_UpdateActorBloggerUrl() {
  local -r id="${1}"
  local -r new_url_unsanitized="${2}"
  local -r FUNC="${FUNCNAME[0]}"

  if [[ -z "${id}" || -z "${new_url_unsanitized}" ]]; then
    EchoE "${FUNC}:${LINENO}: Missing ID or new Blogger URL."
    return 1
  fi

  DBMovie_InitActorDb || return 1

  DBMovie_LoadActorRecord "${id}" >/dev/null || {
    EchoE "${FUNC}:${LINENO}: Cannot update Blogger URL — actor ID '${id}' not found."
    return 1
  }

  local -a fields
  IFS='|' read -r -a fields <<< "${ACTOR_RECORD_LOADED}"

  # Ensure 9 fields (TMDB|IMDB|NAME|DOB|DOD|GENDER|DEPARTMENT|PHOTO_URL|BLOGGER_URL)
  while (( ${#fields[@]} < 9 )); do
    fields+=("")
  done

  # Save updated record  
  DBMovie_SaveActorRecord \
    "${fields[0]}" \
    "${fields[1]}" \
    "${fields[2]}" \
    "${fields[3]}" \
    "${fields[4]}" \
    "${fields[5]}" \
    "${fields[6]}" \
    "${fields[7]}" \
    "$(SanitizeField "${new_url_unsanitized}")"

  return 0
}

#==============================================================================
# DBMovie_GetActorBloggerUrl
#
# Return the Blogger URL for an actor given TMDb ID or IMDb ID.
#
# Globals Used:
#   - ACTOR_DB_PATH
#   - ACTOR_RECORD_LOADED
#
# Arguments:
#   ${1} - TMDb ID or IMDb ID
#
# Outputs:
#   - Echoes the Blogger URL (9th field) if found
#   - Returns 1 if not found
#
# Return:
#   0 - Success (Blogger URL printed)
#   1 - Failure (actor not found or missing URL)
#==============================================================================
DBMovie_GetActorBloggerUrl() {
  local -r id="${1}"
  local -r file="${ACTOR_DB_PATH}"

  if [[ -z "${id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: No ID provided."
    return 1
  fi

  DBMovie_InitActorDb || return 1

  DBMovie_LoadActorRecord "${id}" >/dev/null || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Cannot load actor record for ID '${id}'."
    return 1
  }

  local -a fields
  IFS='|' read -r -a fields <<< "${ACTOR_RECORD_LOADED}"

  # Ensure at least 9 fields
  while (( ${#fields[@]} < 9 )); do
    fields+=("")
  done

  local -r blogger_url="${fields[8]}"

  if [[ -n "${blogger_url}" ]]; then
    echo "${blogger_url}"
    return 0
  else
    return 1
  fi
}

#==============================================================================
# DBMovie_UpdateActorPhotoUrl
#
# Update the photo URL field for an existing actor record.
#
# Globals Used:
#   - ACTOR_DB_PATH
#   - ACTOR_RECORD_LOADED
#
# Globals Modified:
#   - actor.db (one record updated)
#
# Arguments:
#   ${1} - TMDb ID or IMDb ID
#   ${2} - New photo URL (unsanitized)
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_UpdateActorPhotoUrl() {
  local -r id="${1}"
  local -r new_url="${2}"
  local -r FUNC="${FUNCNAME[0]}"

  if [[ -z "${id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: No ID provided."
    return 1
  fi

  DBMovie_InitActorDb || return 1

  DBMovie_LoadActorRecord "${id}" >/dev/null || {
    EchoE "${FUNC}:${LINENO}: Cannot update photo URL: actor not found."
    return 1
  }

  local -a fields
  IFS='|' read -r -a fields <<< "${ACTOR_RECORD_LOADED}"

  # Ensure fields array has at least 9 elements (now includes Gender, Department)
  while (( ${#fields[@]} < 9 )); do
    fields+=("")
  done

  DBMovie_SaveActorRecord \
    "${fields[0]}" \
    "${fields[1]}" \
    "${fields[2]}" \
    "${fields[3]}" \
    "${fields[4]}" \
    "${fields[5]}" \
    "${fields[6]}" \
    "$(SanitizeField "${new_url}")" \
    "${fields[8]}" 

  return 0
}

#==============================================================================
# DBMovie_GetActorPhotoUrl: Retrieve actor photo URL from actor.db
#------------------------------------------------------------------------------
# Description:
#   Given either a TMDb ID or IMDb ID, this function retrieves the actor's
#   photo URL (field 8) by loading the record via DBMovie_LoadActorRecord.
#
# Globals:
#   ACTOR_DB_PATH     - Path to actor.db (used internally)
#   ACTOR_RECORD_LOADED - Set by DBMovie_LoadActorRecord
#
# Arguments:
#   ${1} - TMDb ID or IMDb ID
#
# Outputs:
#   - Echoes the photo URL (field 8) to stdout
#   - Returns 0 if found, 1 on failure
#==============================================================================
DBMovie_GetActorPhotoUrl() {
  local -r input_id="${1:-}"

  if [[ -z "${input_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: No ID provided."
    return 1
  fi

  DBMovie_InitActorDb || return 1

  if ! DBMovie_LoadActorRecord "${input_id}" 2>/dev/null; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to load actor record for ID '${input_id}'."
    return 1
  fi

  local -a fields
  IFS='|' read -r -a fields <<< "${ACTOR_RECORD_LOADED}"

  # Ensure at least 9 fields (TMDB, IMDB, NAME, DOB, DOD, GENDER, DEPARTMENT, PHOTO_URL, BLOGGER_URL)
  while (( ${#fields[@]} < 9 )); do
    fields+=("")
  done

  local -r photo_url="${fields[7]}"  # Field 8 is PHOTO_URL

  if [[ -n "${photo_url}" ]]; then
    echo "${photo_url}"
    return 0
  fi

  return 1
}

#==============================================================================
# DBMovie_IsActorCached
#
# Check if an actor record exists in actor.db by TMDb ID or IMDb ID.
# Tries TMDb ID first (primary key), then IMDb ID (secondary key).
#
# Globals used:
#   - ACTOR_DB_PATH
#
# Arguments:
#   ${1} - Actor ID (TMDb ID preferred, or IMDb ID as fallback)
#
# Return:
#   0 - Found
#   1 - Not found
#==============================================================================
DBMovie_IsActorCached() {
  local -r actor_id="${1:-}"
  local -r file="${ACTOR_DB_PATH}"

  if [[ -z "${actor_id}" ]]; then
    return 1
  fi

  DBMovie_InitActorDb || return 1

  # First try exact TMDb ID match (2nd column)
  if awk -F'|' -v id="${actor_id}" 'NR>1 && $2==id {exit 0} END {exit 1}' "${file}"; then
    return 0
  fi

  # Then try exact IMDb ID match (1st column)
  if awk -F'|' -v id="${actor_id}" 'NR>1 && $1==id {exit 0} END {exit 1}' "${file}"; then
    return 0
  fi

  return 1
}

#==============================================================================
# GetActorTmdbIdFromName
#
# Resolve actor name to TMDb ID, using local cache first. Saves multiple matches
# found online immediately to the local actor.db cache to avoid future API hits.
# If multiple matches exist, asks user to select one.
#
# Arguments:
#   ${1} - Actor name (e.g., "William Powell")
#
# Globals Used:
#   - ACTOR_DB_PATH
#   - TMDB_API_KEY
#
# Globals Modified:
#   - ACTOR_TMDB_ID_RESULT
#   - _ACTOR_SEARCH_RESULT_JSON
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
GetActorTmdbIdFromName() {
  local actor_name="$(SanitizeField "${1:-}")"
  local -a matches
  local line name match_tmdb_id local query_result count
  local -r FUNC="${FUNCNAME[0]}"
  
  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: $*"

  if [[ -z "${actor_name}" ]]; then
    EchoE "${FUNC}:${LINENO}: Actor name is empty."
    return 1
  fi

  DBMovie_InitActorDb || return 1

  local reload_online=false
  while true; do
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: reload_online=${reload_online}"
    matches=()
    if [[ ${reload_online} == false ]]; then
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
      while IFS='|' read -r tmdb_id imdb_id name _; do
        if [[ "${name,,}" == "${actor_name,,}" && "${tmdb_id}" =~ ^[0-9]+$ ]]; then
          matches+=("${tmdb_id}")
        fi
      done < "${ACTOR_DB_PATH}"
    fi

    if (( ${#matches[@]} == 1 )); then
      ACTOR_TMDB_ID_RESULT="${matches[0]}"
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found single match in cache: TMDb ID ${ACTOR_TMDB_ID_RESULT}"

    elif (( ${#matches[@]} > 1 )); then
      ACTOR_TMDB_ID_RESULT="${MANY_RECORD_MARK}"
      EchoW "${FUNC}:${LINENO}: Multiple matches found locally for '${actor_name}'."

    else
      EchoW "${FUNC}:${LINENO}: No local match for '${actor_name}', querying TMDb..."

      query_result="$(curl --silent --fail \
        "https://api.themoviedb.org/3/search/person?api_key=${TMDB_API_KEY}&query=$(urlencode "${actor_name}")")" || {
          EchoE "${FUNC}:${LINENO}: Failed to contact TMDb."
          return 1
      }

      count=$(jq '.results | length' <<< "${query_result}")
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}: count=${count}"

      if (( count == 0 )); then
        ACTOR_TMDB_ID_RESULT=""
        EchoW "${FUNC}:${LINENO}: No actor found on TMDb."
        return 1
      elif (( count >= 1 )); then
        _ACTOR_SEARCH_RESULT_JSON="${query_result}"
        local i tmdb_id imdb_id name dob dod photo_url gender department
        matches=()

        for (( i = 0; i < count; i++ )); do
          tmdb_id=$(jq -r ".results[${i}].id // empty" <<< "${query_result}")
          name=$(jq -r ".results[${i}].name // empty" <<< "${query_result}")

          # Fetch detailed info
          full_info=$(curl --silent --fail "https://api.themoviedb.org/3/person/${tmdb_id}?api_key=${TMDB_API_KEY}") || {
            EchoW "${FUNC}:${LINENO}: Failed to fetch detailed info for TMDb ID ${tmdb_id}."
            continue
          }

          dob=$(jq -r '.birthday // empty' <<< "${full_info}")
          dod=$(jq -r '.deathday // empty' <<< "${full_info}")
          gender=$(jq -r '.gender // empty' <<< "${full_info}")
          department=$(jq -r '.known_for_department // empty' <<< "${full_info}")
          photo_url=$(jq -r '.profile_path // empty' <<< "${full_info}")
          imdb_id=$(jq -r '.imdb_id // empty' <<< "${full_info}")

          matches+=("${tmdb_id}")

          # Fix photo URL
          [[ -n "${photo_url}" && "${photo_url}" != "null" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"

          (( DEBUG )) && EchoD "${FUNC}:${LINENO}: '${tmdb_id}' '${imdb_id}' '${name}'"

          if [[ -n "${tmdb_id}" && -n "${name}" ]]; then
            DBMovie_SaveActorRecord \
              "${tmdb_id}" \
              "${imdb_id}" \
              "${name}" \
              "${dob}" \
              "${dod}" \
              "${gender}" \
              "${department}" \
              "${photo_url}" \
              "" || {
              EchoW "${FUNC}:${LINENO}: Failed to save actor '${name}' (TMDb ID=${tmdb_id}) to cache."
            }
          fi
        done

        if (( count == 1 )); then
          ACTOR_TMDB_ID_RESULT="$(jq -r '.results[0].id' <<< "${query_result}")"
          (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Single match TMDb ID: ${ACTOR_TMDB_ID_RESULT}"
        else
          ACTOR_TMDB_ID_RESULT="${MANY_RECORD_MARK}"
          EchoW "${FUNC}:${LINENO}: Multiple matches found online for '${actor_name}'."
        fi
      fi
    fi

    if [[ "${ACTOR_TMDB_ID_RESULT}" != "${MANY_RECORD_MARK}" ]]; then
      return 0
    fi
    local -a ids names departments
    local -i idx=0

    # Rebuild ids[], dobs[], dods[], genders[], names[], departments[] from local cache
    while IFS='|' read -r tmdb_id imdb_id name dob dod gender department photo_url blog_url; do
      for match in "${matches[@]}"; do
        if [[ "${match}" == "${tmdb_id}" ]]; then
          ids[idx]="${tmdb_id}"
          dobs[idx]="${dob}"
          dods[idx]="${dod}"
          genders[idx]="${gender}"
          names[idx]="${name}"
          departments[idx]="${department}"
          ((idx++))
          break
        fi
      done
    done < "${ACTOR_DB_PATH}"

    if (( idx == 0 )); then
      EchoE "${FUNC}:${LINENO}: No valid actor entries to select."
      return 1
    fi

    Echo
    Echo "Idx  TMDB_ID  DoB        DoD        Gender     Department         Name"
    Echo "---  -------  ---------- ---------- ---------- ----------------- ----------------------------"
    for ((i = 0; i < idx; i++)); do
      local gender_label=
      case "${genders[i]:-2}" in
      0) gender_label="Male" ;;
      1) gender_label="Female" ;;
      *) gender_label="Unknown" ;;
      esac
      Echo "$(printf "%-4s %-8s %-10s %-10s %-10s %-18s %s\n" \
        "${i}" \
        "${ids[i]}" \
        "${dobs[i]:-N/A}" \
        "${dods[i]:-N/A}" \
        "${gender_label}" \
        "${departments[i]:-N/A}" \
        "${names[i]:-Unknown}")"
    done
    Echo
    Echo "To skip this selection, next time use --tmdb <code> or --imdb <code>"
    Echo "To force reload of names from online, use --force-name-online"

    local choice=""
    while true; do
      EchoN "Select the correct TMDb ID [0-$((idx-1))], 'r' to force online reload, or 'q' to quit: "
      read -r choice
      choice="${choice,,}"
      if [[ "${choice}" =~ ^[0-9]+$ ]] && (( choice >= 0 && choice < idx )); then
        ACTOR_TMDB_ID_RESULT="${ids[choice]}"
        Echo "✅ Using TMDb ID ${ACTOR_TMDB_ID_RESULT}"
        return 0
      elif [[ "${choice}" == r ]]; then
        reload_online=true
        break
      elif [[ "${choice}" == q ]]; then
        Echo "❌ Cancelled by user."
        return 1
      fi
      EchoW "Invalid input. Please try again."
    done
  done
}

#-----------------------------------------------------------------------------#
#                                                                             |
#                                                                             |
#                                M O V I E                                    |
#                                                                             |
#                                                                             |
#-----------------------------------------------------------------------------#

#==============================================================================
# ResolveMovieTmdbIdFromImdb
#
# Resolves a TMDb movie ID from a given IMDb ID using the TMDb API.
#
# Arguments:
#   $1 - IMDb movie ID (e.g., tt0033467)
#
# Globals Used:
#   TMDB_API_KEY
#
# Returns:
#   0 on success (prints TMDb ID to stdout)
#   1 on failure
#==============================================================================
ResolveMovieTmdbIdFromImdb() {
  local -r imdb_id="${1:-}"
  local -r FUNC="${FUNCNAME[0]}"

  if [[ -z "${imdb_id}" ]]; then
    EchoE "${FUNC}:${LINENO}: IMDb ID is required."
    return 1
  fi

  if [[ ! "${imdb_id}" =~ ^tt[0-9]+$ ]]; then
    EchoE "${FUNC}:${LINENO}: Invalid IMDb ID format '${imdb_id}'"
    return 1
  fi

  local -r url="https://api.themoviedb.org/3/find/${imdb_id}?api_key=${TMDB_API_KEY}&external_source=imdb_id"
  local response=""
  response="$(curl --silent --fail "${url}")" || {
    EchoE "${FUNC}:${LINENO}: Failed to fetch from TMDb for IMDb ID ${imdb_id}"
    return 1
  }

  local -r tmdb_id="$(jq -r '.movie_results[0].id // empty' <<< "${response}")"

  if [[ -z "${tmdb_id}" ]]; then
    EchoE "${FUNC}:${LINENO}: No TMDb match found for IMDb ID ${imdb_id}"
    return 1
  fi

  EchoD "${FUNC}:${LINENO}: IMDb ${imdb_id} → TMDb ${tmdb_id}"
  echo "${tmdb_id}"
  return 0
}

#==============================================================================
# DBMovie_InitMovieDb
#
# Initialize movie.db if missing or empty. Ensures correct field structure.
#
# Globals used:
#   - MOVIE_DB_PATH
#   - DEBUG
#
# Outputs:
#   - Creates movie.db with a header line if the file is missing or empty.
#   - Logs a debug message on creation if DEBUG is set.
#
# Return:
#   0 - Success
#   1 - Failure (unable to create file)
#==============================================================================
DBMovie_InitMovieDb() {
  local -r file="${MOVIE_DB_PATH}"

  if [[ ! -s "${file}" ]]; then
    mkdir -p "$(dirname "${file}")" 2>/dev/null
    if [[ ! -s "${file}" ]]; then
      echo "TMDB_ID|IMDB_ID|COUNTRY|TITLE_EN|TITLE_ORIG|RELEASE_DATE|WATCHED_DATE|BLOGGER_URL|PHOTO_URL" > "${file}" || {
        EchoE "${FUNCNAME[0]}:${LINENO}: Failed to create ${file}."
        return 1
      }
      (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Created ${file} with correct header."
    fi
  fi

  return 0
}

#==============================================================================
# DBMovie_SaveMovieRecord
#
# Save or update a movie record in movie.db.
# Overwrites existing TMDb or IMDb ID match, or appends new record.
#
# Globals used:
#   - MOVIE_DB_PATH
#
# Arguments:
#   ${1} - TMDb ID
#   ${2} - IMDb ID
#   ${3} - Country (ISO 2-letter code)
#   ${4} - English Title
#   ${5} - Original Title
#   ${6} - Release Date (YYYY-MM-DD)
#   ${7} - Watched Date (optional)
#   ${8} - Blogger URL (optional)
#   ${9} - Photo URL (optional)
#
# Outputs:
#   Updates movie.db safely
#   Logs errors or debug info
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_SaveMovieRecord() {
  local tmdb_id="${1}"
  local imdb_id="${2}"
  local -r country="${3}"
  local title_en="${4}"
  local title_orig="${5}"
  local -r release_date="${6}"
  local -r watched_date="${7}"
  local -r blogger_url="${8}"
  local -r photo_url="${9}"
  local -r file="${MOVIE_DB_PATH}"
  local -r FUNC="${FUNCNAME[0]}"

  # Sanitize fields
  tmdb_id="$(SanitizeField "${tmdb_id}")"
  imdb_id="$(SanitizeField "${imdb_id}")"
  title_en="$(SanitizeField "${title_en}")"
  title_orig="$(SanitizeField "${title_orig}")"
  local -r clean_country="$(SanitizeField "${country}")"
  local -r clean_release="$(SanitizeField "${release_date}")"
  local -r clean_watched="$(SanitizeField "${watched_date}")"
  local -r clean_blogger="$(SanitizeField "${blogger_url}")"
  local -r clean_photo="$(SanitizeField "${photo_url}")"

  # Ensure database file exists
  DBMovie_InitMovieDb || {
    EchoE "${FUNC}:${LINENO}: Failed to initialize movie.db"
    return 1
  }
  
  if [[ "${tmdb_id}" == tt* ]]; then
    local t="${tmdb_id}"
    tmdb_id="${imdb_id}"
    imdb_id="${t}"
  fi
  [[ "${tmdb_id}" == tt* ]] && {
    EchoE "${FUNC}:${LINENO}: Invalid TMDB id '${tmdb_id}'"
    return 1
  }
  [[ -n "${imdb_id}" && "${imdb_id}" != tt* ]] && {
    EchoE "${FUNC}:${LINENO}: Invalid IMDB id '${imdb_id}'"
    return 1
  }
  [[ -z "${tmdb_id}" && -z "${imdb_id}" ]] && {
    EchoE "${FUNC}:${LINENO}: Both TMDB and IMDB are null."
    return 1
  }

  # Remove any existing entry
  grep -v -E "^${tmdb_id}\|" "${file}" | grep -v -E "^\w+\|${tmdb_id}\|" > "${file}.tmp" 2>/dev/null || true

  # Append new record
  echo "${tmdb_id}|${imdb_id}|${clean_country}|${title_en}|${title_orig}|${clean_release}|${clean_watched}|${clean_blogger}|${clean_photo}" >> "${file}.tmp"

  # Replace database
  mv "${file}.tmp" "${file}" || {
    EchoE "${FUNC}:${LINENO}: Failed to update ${file}"
    return 1
  }

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Saved movie: ${tmdb_id} (${title_en})"
  return 0
}

#==============================================================================
# DBMovie_LoadMovieRecordOnline
#
# Fetch a movie's metadata online from TMDb and cache it locally.
#
# Globals used:
#   - TMDB_API_KEY
#   - MOVIE_DB_PATH
#
# Arguments:
#   ${1} - Movie ID (TMDb preferred, or IMDb fallback if needed)
#
# Outputs:
#   - Updates movie.db by calling DBMovie_SaveMovieRecord
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_LoadMovieRecordOnline() {
  local -r movie_id="$1"

  if [[ -z "${movie_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Missing movie ID."
    return 1
  fi

  local movie_json=""
  movie_json=$(curl --silent --fail \
    "https://api.themoviedb.org/3/movie/${movie_id}?api_key=${TMDB_API_KEY}") || {
      EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch movie metadata from TMDb."
      return 1
  }

  local tmdb_id imdb_id country title_en title_orig release_date
  tmdb_id=$(jq -r '.id // empty' <<< "${movie_json}")
  imdb_id=$(jq -r '.imdb_id // empty' <<< "${movie_json}")
  title_en=$(jq -r '.title // empty' <<< "${movie_json}")
  title_orig=$(jq -r '.original_title // empty' <<< "${movie_json}")
  release_date=$(jq -r '.release_date // empty' <<< "${movie_json}")
  country=$(jq -r '.production_countries[0].iso_3166_1 // empty' <<< "${movie_json}")

  if [[ -z "${tmdb_id}" || -z "${title_en}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Retrieved movie metadata incomplete."
    return 1
  fi

  export MOVIE_TMDB_ID="${tmdb_id}"
  export MOVIE_IMDB_ID="${imdb_id}"
  export MOVIE_TITLE_EN="${title_en}"
  export MOVIE_TITLE_ORIG="${title_orig}"
  DBMovie_SaveMovieRecord "${tmdb_id}" "${imdb_id}" "${country}" "${title_en}" "${title_orig}" "${release_date}" "" "" "" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to save movie record."
    return 1
  }

  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Saved movie to cache: ${title_en} (${release_date}) [${country}]"
  return 0
}

#==============================================================================
# FetchMovieMetadataFromTmdb
#
# Fetches and caches movie metadata from TMDb and IMDb for a given movie ID.
# If the movie is already present in the local cache (movie.db), it skips fetch.
#
# Arguments:
#   ${1} - TMDb movie ID
#
# Globals used/modified:
#   - TMDB_API_KEY
#   - DBMovie_SaveMovie
#   - DBMovie_HasMovieMetadata
#   - MOVIE_DB_PATH (implicitly via DBMovie_* functions)
#
# Dependencies:
#   - jq
#   - curl
#
# Return:
#   0 - Success
#   1 - Failure or error during TMDb query
#==============================================================================
#FetchMovieMetadataFromTmdb() {
#  local -r movie_id="${1}"
#  local movie_json imdb_id title original_title release_date country
#
#  if [[ -z "${movie_id}" ]]; then
#    EchoE "${FUNCNAME[0]}:${LINENO}: Missing movie ID."
#    return 1
#  fi
#
#  if DBMovie_IsMovieCached "${movie_id}"; then
#    (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}:✅ Movie ID ${movie_id} already cached."
#    return 0
#  fi
#
#  (( VERBOSE )) && Echo "📡 Fetching movie metadata for TMDb ID ${movie_id}..."
#
#  movie_json=$(curl --silent --fail "https://api.themoviedb.org/3/movie/${movie_id}?api_key=${TMDB_API_KEY}")
#
#  if [[ -z "${movie_json}" ]]; then
#    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to retrieve movie metadata from TMDb."
#    return 1
#  fi
#
#  imdb_id=$(jq -r '.imdb_id // empty' <<< "${movie_json}")
#  title=$(jq -r '.title // empty' <<< "${movie_json}")
#  original_title=$(jq -r '.original_title // empty' <<< "${movie_json}")
#  release_date=$(jq -r '.release_date // empty' <<< "${movie_json}")
#  country=$(jq -r '.production_countries[0].iso_3166_1 // empty' <<< "${movie_json}")
#
#  DBMovie_SaveMovieRecord "${imdb_id}" "${movie_id}" "${country}" "${title}" "${original_title}" "${release_date}" "" "" ""
#
#  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}:🆕 Movie saved to cache: ${title} (${release_date}) [${country}]"
#  
#  return 0
#}

#==============================================================================
# DBMovie_LoadMovieRecord
#
# Load a movie record from movie.db by TMDb ID or IMDb ID.
# If not found locally, attempts to fetch the movie metadata online.
#
# Globals used:
#   - MOVIE_DB_PATH
#   - MOVIE_RECORD_LOADED (exported result)
#
# Arguments:
#   ${1} - Movie ID (TMDb preferred, IMDb fallback)
#
# Outputs:
#   - Sets MOVIE_RECORD_LOADED
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_LoadMovieRecord() {
  local -r movie_id="$(SanitizeField "$1")"
  local record=""
  local -i nPass=0
  local -r FUNC="${FUNCNAME[0]}"

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: movie_id='${movie_id}'"

  if [[ -z "${movie_id}" ]]; then
    EchoE "${FUNC}:${LINENO}: Missing movie ID."
    return 1
  fi

  if [[ ! -f "${MOVIE_DB_PATH}" ]]; then
    EchoE "${FUNC}:${LINENO}: movie.db not found."
    return 1
  fi

  while [[ ${nPass} -lt 2 ]]; do
    # Try TMDb ID first
    record=$(grep -E "^${movie_id}\|" "${MOVIE_DB_PATH}") || \
    record=$(grep -E "^\w+\|${movie_id}\|" "${MOVIE_DB_PATH}") || true

    if [[ -n "${record}" ]]; then
      export MOVIE_RECORD_LOADED="${record}"
      IFS='|' read -r \
        MOVIE_TMDB_ID \
        MOVIE_IMDB_ID \
        MOVIE_COUNTRY \
        MOVIE_TITLE_EN \
        MOVIE_TITLE_ORIG \
        MOVIE_RELEASE_DATE \
        MOVIE_EXISTING_WATCHED_DATE \
        MOVIE_BLOGGER_URL \
        MOVIE_PHOTO_URL <<< "${record}"
      export MOVIE_TMDB_ID
      export MOVIE_IMDB_ID
      export MOVIE_COUNTRY
      export MOVIE_TITLE_EN 
      export MOVIE_TITLE_ORIG 
      export MOVIE_RELEASE_DATE 
      export MOVIE_EXISTING_WATCHED_DATE 
      export MOVIE_BLOGGER_URL 
      export MOVIE_PHOTO_URL
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found movie record: ${record}"
      echo "${record}"
      return 0
    fi

    if (( nPass == 0 )); then
      EchoW "${FUNC}:${LINENO}: Movie ID ${movie_id} not found locally. Attempting online fetch..."
      DBMovie_LoadMovieRecordOnline "${movie_id}" || {
        EchoE "${FUNC}:${LINENO}: Cannot fetch movie metadata online."
        return 1
      }
    fi
    ((nPass++))
  done

  EchoE "${FUNC}:${LINENO}: Movie ID ${movie_id} not found after online fetch."
  return 1
}

#==============================================================================
# DBMovie_IsMovieCached
#
# Check if a movie record exists in movie.db by TMDb ID or IMDb ID.
# Tries TMDb ID first (primary key), then IMDb ID (secondary key).
#
# Globals used:
#   - MOVIE_DB_PATH
#
# Arguments:
#   ${1} - Movie ID (TMDb ID preferred, or IMDb ID as fallback)
#
# Return:
#   0 - Found
#   1 - Not found
#==============================================================================
DBMovie_IsMovieCached() {
  local -r movie_id="$1"
  local -r file="${MOVIE_DB_PATH}"

  if [[ ! -f "${file}" ]]; then
    return 1
  fi

  # First try TMDb ID (primary key match)
  if grep -q -E "^${movie_id}\|" "${file}"; then
    return 0
  fi

  # Then try IMDb ID (secondary key match)
  if grep -q -E "^\w+\|${movie_id}\|" "${file}"; then
    return 0
  fi

  return 1
}

#==============================================================================
# DBMovie_UpdateWatchedDate
#
# Update the WATCHED_DATE field in movie.db for a given TMDb ID.
# Loads the record, modifies WATCHED_DATE, and saves it via DBMovie_SaveMovieRecord.
#
# Globals used:
#   - MOVIE_DB_PATH
#
# Arguments:
#   ${1} - TMDb movie ID (primary key)
#   ${2} - Watched date (YYYY-MM-DD, optional; defaults to today)
#
# Outputs:
#   - Updates movie.db via DBMovie_SaveMovieRecord
#   - Logs errors via EchoE
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_UpdateWatchedDate() {
  local -r movie_id="${1}"
  local -r watched_date="${2:-$(date +%F)}"
  local -r file="${MOVIE_DB_PATH}"

  if [[ -z "${movie_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Missing TMDb movie ID."
    return 1
  fi

  if [[ ! -f "${file}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: movie.db not found: ${file}"
    return 1
  fi

  # Load the record
  local record
  record=$(grep -E "^${movie_id}\|" "${file}") || record=$(grep -E "^\w+\|${movie_id}\|" "${file}") || true

  if [[ -z "${record}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Movie ID ${movie_id} not found in movie.db."
    return 1
  fi

  # Split fields
  IFS='|' read -r tmdb_id imdb_id country title_en title_orig release_date existing_watched_date blogger_url photo_url <<< "${record}"

  # Save updated record
  DBMovie_SaveMovieRecord "${tmdb_id}" "${imdb_id}" "${country}" "${title_en}" "${title_orig}" "${release_date}" "${watched_date}" "${blogger_url}" "${photo_url}" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to save updated movie record."
    return 1
  }

  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Updated WATCHED_DATE for TMDb ID ${tmdb_id} to ${watched_date}"
  return 0
}

#==============================================================================
# DBMovie_GetMovieWatchedDate
#
# Retrieve the WATCHED_DATE field from movie.db for a given TMDb or IMDb ID.
# Tries TMDb ID match first, then IMDb ID match.
#
# Globals used:
#   - MOVIE_DB_PATH
#
# Arguments:
#   ${1} - Movie ID (TMDb preferred, IMDb fallback)
#
# Outputs:
#   - Echoes the WATCHED_DATE (YYYY-MM-DD) to stdout
#   - Echoes nothing if movie not found
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_GetMovieWatchedDate() {
  local -r movie_id="${1}"
  local record=""

  if [[ -z "${movie_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Missing movie ID."
    return 1
  fi

  if [[ ! -f "${MOVIE_DB_PATH}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: movie.db not found."
    return 1
  fi

  # Try TMDb ID match first
  record=$(grep -E "^${movie_id}\|" "${MOVIE_DB_PATH}") || \
  record=$(grep -E "^\w+\|${movie_id}\|" "${MOVIE_DB_PATH}") || true

  if [[ -n "${record}" ]]; then
    IFS='|' read -r _ _ _ _ _ _ watched_date _ _ <<< "${record}"
    echo "${watched_date}"
    (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Found WATCHED_DATE=${watched_date} for ID=${movie_id}"
  fi

  return 0
}

#==============================================================================
# DBMovie_DeduplicateMovieDb
#
# Cleans up duplicate TMDb or IMDb entries in movie.db.
# Keeps only the latest occurrence for any TMDb or IMDb ID.
#
# Globals used:
#   - MOVIE_DB_PATH
#
# Arguments:
#   None
#
# Outputs:
#   - Overwrites movie.db with deduplicated content
#   - Logs progress and errors via Echo/EchoD/EchoE
#
# Return:
#   0 - Success
#   1 - Failure (movie.db not found)
#==============================================================================
DBMovie_DeduplicateMovieDb() {
  local -r file="${MOVIE_DB_PATH}"
  local -r tmpfile="${file}.dedup.tmp"

  if [[ ! -f "${file}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: ❌ movie.db not found: ${file}"
    return 1
  fi

  Echo "${FUNCNAME[0]}:${LINENO}: 🧹 Deduplicating movie.db at ${file}..."

  # Preserve header
  head -n 1 "${file}" > "${tmpfile}" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to copy header to temporary file."
    return 1
  }

  # Deduplicate: prefer last occurrence of TMDb or IMDb ID
  tail -n +2 "${file}" | tac | awk -F'|' '
    {
      tmdb = $1;
      imdb = $2;
      if (!(tmdb in seen_tmdb) && !(imdb in seen_imdb)) {
        lines[NR] = $0;
        seen_tmdb[tmdb] = 1;
        seen_imdb[imdb] = 1;
      }
    }
    END {
      for (i = NR; i >= 1; i--) {
        if (i in lines) print lines[i];
      }
    }
  ' >> "${tmpfile}" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed during deduplication pass."
    rm -f "${tmpfile}"
    return 1
  }

  # Replace original file
  mv "${tmpfile}" "${file}" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to replace original movie.db with deduplicated version."
    return 1
  }

  Echo "${FUNCNAME[0]}:${LINENO}: ✅ movie.db has been deduplicated successfully."
  return 0
}

#-----------------------------------------------------------------------------#
#                                                                             |
#                                                                             |
#                        M O V I E   C R E D I T                              |
#                                                                             |
#                                                                             |
#-----------------------------------------------------------------------------#

#==============================================================================
# DBMovie_InitMovieCreditsDb
#
# Initializes the movie_credits.db file if missing or empty.
# Creates a header with full Actor and Movie TMDb/IMDb keys plus Role.
#
# Globals:
#   MOVIE_CREDITS_DB_PATH - Full path to movie_credits.db
#
# Outputs:
#   - Creates the file if missing/empty
#   - Logs debug message if DEBUG is enabled
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_InitMovieCreditsDb() {
  local -r file="${MOVIE_CREDITS_DB_PATH}"

  if [[ ! -s "${file}" ]]; then
    mkdir -p "$(dirname "${file}")" 2>/dev/null
    if [[ ! -s "${file}" ]]; then
      echo "ACTOR_TMDB_ID|ACTOR_IMDB_ID|MOVIE_TMDB_ID|MOVIE_IMDB_ID|ROLE" > "${file}" || {
        EchoE "${FUNCNAME[0]}:${LINENO}: Failed to create ${file}."
        return 1
      }
      (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Created ${file} with header."
    fi
  fi

  return 0
}

#==============================================================================
# DBMovie_SaveMovieCredit
#
# Save or update a movie credit record in movie_credits.db.
# Now stores both TMDb and IMDb IDs for movies and actors.
#
# Globals used:
#   - MOVIE_CREDITS_DB_PATH
#
# Arguments:
#   ${1} - Movie ID (TMDb or IMDb)
#   ${2} - Actor ID (TMDb or IMDb)
#   ${3} - Role (character name)
#
# Outputs:
#   - Updates movie_credits.db safely
#   - Logs debug info via EchoD
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_SaveMovieCredit() {
  local -r movie_id="$1"
  local -r actor_id="$2"
  local role="$3"
  local -r file="${MOVIE_CREDITS_DB_PATH}"
  local -r FUNC="${FUNCNAME[0]}"

  DEBUG=1

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: movie_id=${movie_id}  actor_id=${actor_id} "

  # Sanitize fields
  local -r safe_movie_id="$(SanitizeField "${movie_id}")"
  local -r safe_actor_id="$(SanitizeField "${actor_id}")"
  local -r safe_role="$(SanitizeField "${role}")"

  [[ -z "${safe_movie_id}" || -z "${safe_actor_id}" ]] && {
    EchoE "${FUNC}:${LINENO}: Missing movie ID or actor ID."
    return 1
  }

  # Ensure database file exists
  DBMovie_InitMovieCreditsDb || {
    EchoE "${FUNC}:${LINENO}: Failed to initialize movie_credits.db"
    return 1
  }

  # Resolve all 4 IDs
  local actor_tmdb=""
  local actor_imdb=""
  local movie_tmdb=""
  local movie_imdb=""

  # Lookup movie
  local -r movie_record="$(DBMovie_LoadMovieRecord "${safe_movie_id}")" || {
    EchoE "${FUNC}:${LINENO}: Movie ID '${safe_movie_id}' not found."
    return 1
  }
  [[ -z "${movie_record}" ]] && {
    EchoE "${FUNC}:${LINENO}: movie_record empty."
    return 1
  }

  IFS='|' read -r movie_tmdb movie_imdb _ <<< "${movie_record}"
  [[ -z "${movie_tmdb}" && -z "${movie_imdb}" ]] && {
    EchoE "${FUNC}:${LINENO}: Movie ID not defined after movie_record load."
    return 1
  }

  # Lookup actor
  local -r actor_record="$(DBMovie_LoadActorRecord "${safe_actor_id}")" || {
    EchoE "${FUNC}:${LINENO}: Actor ID '${safe_actor_id}' not found."
    return 1
  }
  IFS='|' read -r actor_tmdb actor_imdb _ <<< "${actor_record}"

  # Remove existing record matching Actor + Movie
  awk -F'|' -v at="${actor_tmdb}" -v ai="${actor_imdb}" -v mt="${movie_tmdb}" -v mi="${movie_imdb}" '{
    if (!($1 == at && $2 == ai && $3 == mt && $4 == mi)) print $0;
  }' "${file}" > "${file}.tmp" 2>/dev/null || true

  # Save new record
  echo "${actor_tmdb}|${actor_imdb}|${movie_tmdb}|${movie_imdb}|${safe_role}" >> "${file}.tmp"

  # Replace database
  mv "${file}.tmp" "${file}" || {
    EchoE "${FUNC}:${LINENO}: Failed to update movie_credits.db"
    return 1
  }
  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Saved movie credit: Actor ${actor_tmdb}/${actor_imdb} Movie ${movie_tmdb}/${movie_imdb}"
  return 0
}

#==============================================================================
# DBMovie_LoadMovieCreditByActorIdOnline
#
# Fetches an actor's filmography from TMDb and stores records using
# DBMovie_SaveMovieCredit. Accepts either TMDb or IMDb actor ID.
#
# Arguments:
#   $1 - Actor ID (TMDb numeric or IMDb ID like "nm0000001")
#
# Globals Used:
#   - ACTOR_TMDB_ID
#   - ACTOR_IMDB_ID
#   - TMDB_API_KEY
#
# Returns:
#   0 on success, 1 on failure
#==============================================================================
DBMovie_LoadMovieCreditByActorIdOnline() {
  local -r actor_id_input="${1:-}"
  local -r FUNC="${FUNCNAME[0]}"

  if [[ -z "${actor_id_input}" ]]; then
    EchoE "${FUNC}:${LINENO}: No actor ID provided."
    return 1
  fi

  ResolveActorIds "${actor_id_input}" || {
    EchoE "${FUNC}:${LINENO}: Failed to resolve actor ID '${actor_id_input}'"
    return 1
  }

  local -r tmdb_id="${ACTOR_TMDB_ID}"
  local -r imdb_id="${ACTOR_IMDB_ID}"

  DBMovie_InitMovieCreditsDb || return 1

  local -r url="https://api.themoviedb.org/3/person/${tmdb_id}/movie_credits?api_key=${TMDB_API_KEY}"
  local -r credits_json="$(curl --silent --fail "${url}")" || {
    EchoE "${FUNC}:${LINENO}: Failed to fetch movie credits for actor ${tmdb_id} from TMDb."
    return 1
  }

  Echo "Retriving all movies with actor '${ACTOR_TMDB_ID}'. Use --verbose to see ach movie title."
  local -i count=0
  while read -r entry; do
    local movie_tmdb_id="$(jq -r '.id // empty' <<< "${entry}")"
    local role="$(jq -r '.character // empty' <<< "${entry}")"

    [[ -z "${movie_tmdb_id}" || -z "${role}" ]] && continue

    # Fetch metadata (will automatically cache)
    if DBMovie_LoadMovieRecord "${movie_tmdb_id}" >/dev/null; then
      (( VERBOSE )) && Echo "  🎬 Saving movie credit for movie '${MOVIE_TMDB_ID}' '${MOVIE_TITLE_EN} (${MOVIE_RELEASE_DATE})'"
      DBMovie_SaveMovieCredit "${movie_tmdb_id}" "${tmdb_id}" "${role}" || {
        EchoW "${FUNC}:${LINENO}: Failed to save credit for movie ${movie_tmdb_id}"
      }
      ((count++))
    else
      EchoW "${FUNC}:${LINENO}: Could not fetch metadata for movie ID ${movie_tmdb_id}"
    fi
  done < <(
    jq -c '.cast[] | select(.id and .title and .character)' <<< "${credits_json}"
  )

  if (( count == 0 )); then
    EchoW "${FUNC}:${LINENO}: No usable credits saved for actor ${tmdb_id}"
    return 1
  fi

  Echo "📚 Saved ${count} movie credit(s) for actor ${tmdb_id} (${imdb_id})"
  return 0
}

#==============================================================================
# DBMovie_LoadMovieCreditByActorId
#
# Returns all movie credits for a given actor from the local movie_credits.db.
# If no record is found, attempts to fetch and cache via TMDb API.
#
# Arguments:
#   $1 - Actor TMDb ID or IMDb ID
#
# Globals Used:
#   ACTOR_TMDB_ID
#   MOVIE_CREDITS_DB_PATH
#
# Output:
#   Matching lines from movie_credits.db where actor_id == resolved TMDb ID
#
# Returns:
#   0 on success, 1 on error
#==============================================================================
DBMovie_LoadMovieCreditByActorId() {
  local -r actor_id_input="$(SanitizeField "${1:-}")"
  local -r FUNC="${FUNCNAME[0]}"

  DBMovie_InitMovieCreditsDb || return 1

  if [[ -z "${actor_id_input}" ]]; then
    EchoE "${FUNC}:${LINENO}: No actor ID provided."
    return 1
  fi

  ResolveActorIds "${actor_id_input}" || {
    EchoE "${FUNC}:${LINENO}: Failed to resolve TMDb ID for '${actor_id_input}'"
    return 1
  }

  for (( i=0; i<2; i++ )); do
    # TMDB id is the first field
    local result="$(grep "^${ACTOR_TMDB_ID}|" "${MOVIE_CREDITS_DB_PATH}" || true)"

    if [[ -n "${result}" ]]; then
      echo "${result}"
      return 0
    fi

    if (( i == 0 )); then
      EchoW "${FUNC}:${LINENO}: No local movie credits found for actor ${ACTOR_TMDB_ID}, fetching online..."
      DBMovie_LoadMovieCreditByActorIdOnline "${actor_id_input}" >/dev/null || {
        EchoW "${FUNC}:${LINENO}: Online fetch failed."
        break
      }
    fi
  done

  EchoE "${FUNC}:${LINENO}: No movie credits found for actor ${actor_id_input} after cache and online attempts."
  return 1
}

#==============================================================================
# DBMovie_LoadMovieCreditByMovieId
#
# Returns all actor credits for a given movie from the local movie_credits.db.
#
# Arguments:
#   $1 - Movie TMDb ID or IMDb ID
#
# Globals Used:
#   MOVIE_CREDITS_DB_PATH
#   ACTOR_TMDB_ID (not used here)
#
# Output:
#   Matching lines from movie_credits.db where movie_id == resolved TMDb ID
#
# Returns:
#   0 on success, 1 on error
#==============================================================================
DBMovie_LoadMovieCreditByMovieId() {
  local -r movie_id_input="${1:-}"
  local -r db_file="${MOVIE_CREDITS_DB_PATH}"
  local -r FUNC="${FUNCNAME[0]}"

  DBMovie_InitMovieCreditsDb || {
    EchoE "${FUNC}:${LINENO}: Failed to initialize ${db_file}"
    return 1
  }

  if [[ -z "${movie_id_input}" ]]; then
    EchoE "${FUNC}:${LINENO}: No movie ID provided."
    return 1
  fi

  # Resolve to TMDb ID
  local tmdb_id=""
  if [[ "${movie_id_input}" =~ ^tt[0-9]+$ ]]; then
    tmdb_id="$(ResolveMovieTmdbIdFromImdb "${movie_id_input}")" || {
      EchoE "${FUNC}:${LINENO}: Failed to resolve TMDb ID for IMDb ID '${movie_id_input}'"
      return 1
    }
  else
    tmdb_id="${movie_id_input}"
    if ! DBMovie_HasMovieMetadata "${tmdb_id}"; then
      EchoW "${FUNC}:${LINENO}: Movie ID ${tmdb_id} not in cache. Attempting fetch..."
      DBMovie_LoadMovieRecord "${tmdb_id}" >/dev/null || {
        EchoE "${FUNC}:${LINENO}: Could not fetch or resolve movie metadata."
        return 1
      }
    fi
  fi

  local result=""
  result="$(awk -F'|' -v mid="${tmdb_id}" '$3 == mid { print $0 }' "${db_file}" || true)"

  if [[ -n "${result}" ]]; then
    echo "${result}"
    return 0
  else
    EchoW "${FUNC}:${LINENO}: No credits found for movie ID ${tmdb_id}"
    return 1
  fi
}

#==============================================================================
# DBMovie_DeleteCreditsByActor
#
# Remove all movie credit entries for a given actor (by TMDb or IMDb ID).
#
# Globals used:
#   - MOVIE_CREDITS_DB_PATH
#
# Arguments:
#   ${1} - Actor ID (can be TMDb or IMDb)
#
# Outputs:
#   - Updates movie_credits.db safely
#   - Logs debug info via EchoD
#
# Return:
#   0 - Always (even if no matching entries found)
#==============================================================================
DBMovie_DeleteCreditsByActor() {
  local -r actor_id="$(SanitizeField "${1}")"
  local -r FUNC="${FUNCNAME[0]}"

  if [[ -z "${actor_id}" ]]; then
    EchoE "${FUNC}:${LINENO}: Missing actor ID."
    return 1
  fi

  DBMovie_InitMovieCreditsDb || return 1

  # Load actor record to resolve TMDb and IMDb IDs
  local actor_record="$(DBMovie_LoadActorRecord "${actor_id}")" || {
    EchoE "${FUNC}:${LINENO}: Cannot resolve actor ID '${actor_id}'."
    return 1
  }

  IFS='|' read -r actor_tmdb_id actor_imdb_id _ <<< "${actor_record}"

  if [[ -z "${actor_tmdb_id}" && -z "${actor_imdb_id}" ]]; then
    EchoE "${FUNC}:${LINENO}: Resolved actor IDs are empty."
    return 1
  fi

  # Filter: Remove any line where (Actor_TMDB_ID == tmdb) or (Actor_IMDB_ID == imdb)
  awk -F'|' -v at="${actor_tmdb_id}" -v ai="${actor_imdb_id}" '{
    if (!($1 == at || $2 == ai)) print $0;
  }' "${MOVIE_CREDITS_DB_PATH}" > "${MOVIE_CREDITS_DB_PATH}.tmp" 2>/dev/null || true

  mv "${MOVIE_CREDITS_DB_PATH}.tmp" "${MOVIE_CREDITS_DB_PATH}" || {
    EchoE "${FUNC}:${LINENO}: Failed to update ${MOVIE_CREDITS_DB_PATH}"
    return 1
  }

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Deleted movie credits for actor ${actor_tmdb_id}/${actor_imdb_id}."
  return 0
}

#==============================================================================
# DBMovie_IsCreditCached
#
# Check if a movie credit exists inside movie_credits.db.
# Accepts TMDb or IMDb IDs for both movie and actor.
#
# Globals used:
#   - MOVIE_CREDITS_DB_PATH
#
# Arguments:
#   ${1} - Movie ID (TMDb or IMDb)
#   ${2} - Actor ID (TMDb or IMDb)
#
# Return:
#   0 - Credit found
#   1 - Not found or error
#==============================================================================
DBMovie_IsCreditCached() {
  local -r movie_id="$1"
  local -r actor_id="$2"
  local -r file="${MOVIE_CREDITS_DB_PATH}"

  if [[ -z "${movie_id}" || -z "${actor_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Missing movie or actor ID."
    return 1
  fi

  if [[ ! -f "${file}" ]]; then
    return 1
  fi

  # Resolve full movie and actor records
  local movie_record actor_record
  movie_record="$(DBMovie_LoadMovieRecord "${movie_id}")" || return 1
  actor_record="$(DBMovie_LoadActorRecord "${actor_id}")" || return 1

  local movie_tmdb movie_imdb actor_tmdb actor_imdb
  IFS='|' read -r movie_tmdb movie_imdb _ <<< "${movie_record}"
  IFS='|' read -r actor_tmdb actor_imdb _ <<< "${actor_record}"

  # Search credit: match either TMDb or IMDb IDs
  awk -F'|' -v at="${actor_tmdb}" -v ai="${actor_imdb}" -v mt="${movie_tmdb}" -v mi="${movie_imdb}" '{
    if (($1 == at || $2 == ai) && ($3 == mt || $4 == mi)) {
      exit 0
    }
  }
  END { exit 1 }
  ' "${file}"
}

#==============================================================================
# DBMovie_LoadMoviesByActor
#
# Outputs all movie.db lines corresponding to a given actor (TMDb or IMDb ID).
# Resolves IDs using cache first. Ensures safe output, with full matching lines.
#
# Globals used:
#   - MOVIE_DB_PATH
#   - MOVIE_CREDITS_DB_PATH
#
# Arguments:
#   ${1} - Actor ID (TMDb or IMDb)
#
# Outputs:
#   Echoes full lines from movie.db to stdout
#
# Return:
#   0 - Success (even if no movies found)
#   1 - Failure (errors, missing inputs, missing cache)
#==============================================================================
DBMovie_LoadMoviesByActor() {
  local -r actor_id="$1"
  local -r tmp_ids="$(mktemp)"
  
  if [[ -z "${actor_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Missing actor ID."
    rm -f "${tmp_ids}"
    return 1
  fi

  DBMovie_InitMovieDb
  DBMovie_InitMovieCreditsDb

  # Resolve actor IDs
  local actor_record=""
  actor_record="$(DBMovie_LoadActorRecord "${actor_id}")" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Actor '${actor_id}' not found in cache."
    rm -f "${tmp_ids}"
    return 1
  }

  # "ACTOR_TMDB_ID|ACTOR_IMDB_ID|MOVIE_TMDB_ID|MOVIE_IMDB_ID|ROLE"
  local actor_tmdb_id=""
  local actor_imdb_id=""
  IFS='|' read -r actor_tmdb_id actor_imdb_id _ <<< "${actor_record}"

  # Extract matching movie IDs (TMDb or IMDb)
  awk -F'|' -v at="${actor_tmdb_id}" -v ai="${actor_imdb_id}" '{
    if ($1 == at || $2 == ai) print $3;
  }' "${MOVIE_CREDITS_DB_PATH}" > "${tmp_ids}"

  if [[ ! -s "${tmp_ids}" ]]; then
    EchoW "${FUNCNAME[0]}:${LINENO}: No movie credits found for actor ${actor_tmdb_id}/${actor_imdb_id}."
    rm -f "${tmp_ids}"
    return 0
  fi

  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Extracted movie IDs: $(< "${tmp_ids}")"

  # Output matching full movie.db lines
  awk -F'|' 'NR==FNR { ids[$1]; next } $1 in ids' "${tmp_ids}" "${MOVIE_DB_PATH}"

  rm -f "${tmp_ids}"
  return 0
}

#==============================================================================
# DBMovie_GetMovieRole
#
# Retrieves the role an actor played in a specific movie using movie_credits.db.
# Accepts either TMDb or IMDb IDs for both actor and movie.
#
# Format of each line in movie_credits.db:
#   ACTOR_TMDB_ID|ACTOR_IMDB_ID|MOVIE_TMDB_ID|MOVIE_IMDB_ID|ROLE
#
# Arguments:
#   $1 - Movie ID (TMDb numeric or IMDb ID like "tt...")
#   $2 - Actor ID (TMDb numeric or IMDb ID like "nm...")
#
# Globals Used:
#   - MOVIE_CREDITS_DB_PATH
#
# Output:
#   - Role string to stdout (only if found and unambiguous)
#
# Returns:
#   0 - success (role found and unique if both IDs given)
#   1 - failure (missing ID, resolution error, or ambiguity)
#==============================================================================
DBMovie_GetMovieRole() {
  local -r FUNC="${FUNCNAME[0]}"
  local -r movie_input="$(SanitizeField "${1}")"
  local -r actor_input="$(SanitizeField "${2}")"

  if [[ -z "${movie_input}" || -z "${actor_input}" ]]; then
    EchoE "${FUNC}:${LINENO}: Both movie ID and actor ID must be provided."
    return 1
  fi

  DBMovie_InitMovieCreditsDb || return 1

  # Resolve movie and actor TMDb IDs
  local movie_record actor_record movie_tmdb actor_tmdb
  movie_record="$(DBMovie_LoadMovieRecord "${movie_input}")" || {
    EchoE "${FUNC}:${LINENO}: Could not resolve movie ID '${movie_input}'"
    return 1
  }
  actor_record="$(DBMovie_LoadActorRecord "${actor_input}")" || {
    EchoE "${FUNC}:${LINENO}: Could not resolve actor ID '${actor_input}'"
    return 1
  }
  IFS='|' read -r movie_tmdb _ _ <<< "${movie_record}"
  IFS='|' read -r actor_tmdb _ _ <<< "${actor_record}"

  # Match all lines where actor and/or movie match
  local -a matches=()
  local line a_tmdb a_imdb m_tmdb m_imdb role

  while IFS='|' read -r a_tmdb a_imdb m_tmdb m_imdb role; do
    if [[ "${a_tmdb}" == "${actor_tmdb}" && "${m_tmdb}" == "${movie_tmdb}" ]]; then
      matches+=("${role}")
    fi
  done < "${MOVIE_CREDITS_DB_PATH}"

  if (( ${#matches[@]} == 1 )); then
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found role for actor ${actor_tmdb} in movie ${movie_tmdb}: ${matches[0]}"
    echo "${matches[0]}"
    return 0
  elif (( ${#matches[@]} == 0 )); then
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: No matching credit found."
    return 1
  else
    EchoE "${FUNC}:${LINENO}: Multiple entries found for actor ${actor_tmdb} and movie ${movie_tmdb} — expected only one."
    return 1
  fi
}

#==============================================================================
# DBMovie_BuildActorFilmographyTable: Build HTML table rows for actor filmography
#------------------------------------------------------------------------------
# Description:
#   Generates the HTML table rows for an actor's filmography by retrieving
#   movie credits from TMDb using the person/<id>/movie_credits endpoint. For
#   each movie role, it computes the actor's age at release, checks whether
#   the movie was released after the actor's death (marks ❌), and builds a
#   complete <tr> element with:
#     - Release year
#     - Title
#     - Role name
#     - Calculated age
#     - Link to IMDb search for the title
#
#   The generated HTML table is returned as a single string via stdout.
#
# Arguments:
#   ${1} - TMDb actor ID (e.g., 12345)
#
# Globals Used:
#   ACTOR_TMDB_ID     - TMDb actor ID (reference only)
#   ACTOR_BIRTH_YEAR  - Year of birth (YYYY)
#   ACTOR_DOD         - Date of death (YYYY-MM-DD or empty)
#   TMDB_API_KEY      - Required TMDb API key
#   MOVIE_DB_PATH     - Path to local movie metadata file (not used here but assumed related)
#
# Outputs:
#   Echoes a complete HTML <table> element with one row per film
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_BuildActorFilmographyTable() {
  local -r tmdb_id="${1}"
  local output=""
  local -r tmpfile=$(mktemp)
  local -r credits_url="https://api.themoviedb.org/3/person/${tmdb_id}/movie_credits?api_key=${TMDB_API_KEY}"

  curl --silent --fail "${credits_url}" > "${tmpfile}"

  output+="<h2>Filmography</h2>"
  output+="<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">"
  output+="<tr><th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th></tr>"

  while IFS='|' read -r release title role; do
    year="${release:0:4}"
    age="?"
    if [[ "${ACTOR_BIRTH_YEAR}" =~ ^[0-9]+$ && "${year}" =~ ^[0-9]+$ ]]; then
      age=$((year - ACTOR_BIRTH_YEAR))
    fi

    if [[ -n "${ACTOR_DOD}" && "${year}" -gt "${ACTOR_DOD:0:4}" ]]; then
      age='<span style="color:red;">❌</span>'
    fi

    local search_url="https://www.imdb.com/find?q=$(urlencode "${title} ${year}")"
    output+="<tr><td>${year}</td><td>${title}</td><td>${role}</td><td>${age}</td><td><a href=\"${search_url}\" target=\"_blank\">🔍</a></td></tr>"
  done < <(
    jq -r '.cast[] | select(.release_date != null and .title != null) |
             [.release_date, .title, .character] | @tsv' "${tmpfile}"
  )

  output+="</table>"
  rm -f "${tmpfile}"

  echo "${output}"
}

#-----------------------------------------------------------------------------#
#                                                                             |
#                                                                             |
#                                L I N K                                      |
#                                                                             |
#                                                                             |
#-----------------------------------------------------------------------------#

#==============================================================================
# DBMovie_InitLinksDb
#
# Initialize links.db if it does not exist or is empty.
# Creates the file with the proper header line for actor/movie/credit links.
#
# Globals used:
#   - LINKS_DB_PATH
#
# Globals modified:
#   - links.db (created if missing)
#
# Arguments:
#   None
#
# Outputs:
#   - Creates the links.db file if missing or empty
#   - Logs debug message on successful creation if DEBUG is enabled
#
# Return:
#   0 - Success
#   1 - Failure (unable to create file)
#==============================================================================
DBMovie_InitLinksDb() {
  local -r file="${LINKS_DB_PATH}"

  if [[ ! -s "${file}" ]]; then
    mkdir -p "$(dirname "${file}")" 2>/dev/null
    if [[ ! -s "${file}" ]]; then
      echo "TYPE|ACTOR_TMDB_ID|ACTOR_IMDB_ID|MOVIE_TMDB_ID|MOVIE_IMDB_ID|URL" > "${file}" || {
        EchoE "${FUNCNAME[0]}:${LINENO}: Failed to create ${file}."
        return 1
      }
      (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Created ${file} with header."
    fi
  fi

  return 0
}

#==============================================================================
# DBMovie_SaveLinkRecord
#
# Save or update a link entry inside links.db.
# Resolves missing TMDb/IMDb IDs automatically from local caches.
#
# Globals used:
#   - MOVIE_LINKS_DB_PATH
#   - ACTOR_DB_PATH
#   - MOVIE_DB_PATH
#
# Arguments:
#   ${1} - Type (A, M, C)
#   ${2} - TMDb Actor ID (may be empty)
#   ${3} - IMDb Actor ID (may be empty)
#   ${4} - TMDb Movie ID (may be empty)
#   ${5} - IMDb Movie ID (may be empty)
#   ${6} - Full Encoded URL
#
# Outputs:
#   Writes to ${MOVIE_LINKS_DB_PATH}
#   Logs debug info if DEBUG is enabled
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
DBMovie_SaveLinkRecord() {
  local -r type="${1}"
  local tmdb_actor="${2}"
  local imdb_actor="${3}"
  local tmdb_movie="${4}"
  local imdb_movie="${5}"
  local url="${6}"

  local -r file="${MOVIE_LINKS_DB_PATH}"

  # Sanitize URL only
  sanitize() { echo "${1//[$'\t\r\n|']}"; }
  url="$(sanitize "${url}")"

  if [[ -z "${type}" || -z "${url}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Missing type or URL."
    return 1
  fi

  # Resolve missing IDs
  case "${type}" in
    A)
      if [[ -z "${tmdb_actor}" && -z "${imdb_actor}" ]]; then
        EchoE "${FUNCNAME[0]}:${LINENO}: Actor ID (TMDb or IMDb) required for Actor link."
        return 1
      fi
      if [[ -z "${tmdb_actor}" ]]; then
        local record
        record="$(DBMovie_LoadActorRecord "${imdb_actor}")" || {
          EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve TMDb ID from IMDb ID '${imdb_actor}'."
          return 1
        }
        IFS='|' read -r tmdb_actor imdb_actor _ <<< "${record}"
      elif [[ -z "${imdb_actor}" ]]; then
        local record
        record="$(DBMovie_LoadActorRecord "${tmdb_actor}")" || {
          EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve IMDb ID from TMDb ID '${tmdb_actor}'."
          return 1
        }
        IFS='|' read -r tmdb_actor imdb_actor _ <<< "${record}"
      fi
      tmdb_movie=""
      imdb_movie=""
      ;;
    M)
      if [[ -z "${tmdb_movie}" && -z "${imdb_movie}" ]]; then
        EchoE "${FUNCNAME[0]}:${LINENO}: Movie ID (TMDb or IMDb) required for Movie link."
        return 1
      fi
      if [[ -z "${tmdb_movie}" ]]; then
        local record
        record="$(DBMovie_LoadMovieRecord "${imdb_movie}")" || {
          EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve TMDb ID from IMDb ID '${imdb_movie}'."
          return 1
        }
        IFS='|' read -r tmdb_movie imdb_movie _ <<< "${record}"
      elif [[ -z "${imdb_movie}" ]]; then
        local record
        record="$(DBMovie_LoadMovieRecord "${tmdb_movie}")" || {
          EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve IMDb ID from TMDb ID '${tmdb_movie}'."
          return 1
        }
        IFS='|' read -r tmdb_movie imdb_movie _ <<< "${record}"
      fi
      tmdb_actor=""
      imdb_actor=""
      ;;
    C)
      if [[ (-z "${tmdb_actor}" && -z "${imdb_actor}") || (-z "${tmdb_movie}" && -z "${imdb_movie}") ]]; then
        EchoE "${FUNCNAME[0]}:${LINENO}: Both actor and movie IDs required for Credit link."
        return 1
      fi
      # Resolve actor IDs
      if [[ -z "${tmdb_actor}" ]]; then
        local record
        record="$(DBMovie_LoadActorRecord "${imdb_actor}")" || {
          EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve actor TMDb ID from IMDb ID '${imdb_actor}'."
          return 1
        }
        IFS='|' read -r tmdb_actor imdb_actor _ <<< "${record}"
      elif [[ -z "${imdb_actor}" ]]; then
        local record
        record="$(DBMovie_LoadActorRecord "${tmdb_actor}")" || {
          EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve actor IMDb ID from TMDb ID '${tmdb_actor}'."
          return 1
        }
        IFS='|' read -r tmdb_actor imdb_actor _ <<< "${record}"
      fi
      # Resolve movie IDs
      if [[ -z "${tmdb_movie}" ]]; then
        local record
        record="$(DBMovie_LoadMovieRecord "${imdb_movie}")" || {
          EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve movie TMDb ID from IMDb ID '${imdb_movie}'."
          return 1
        }
        IFS='|' read -r tmdb_movie imdb_movie _ <<< "${record}"
      elif [[ -z "${imdb_movie}" ]]; then
        local record
        record="$(DBMovie_LoadMovieRecord "${tmdb_movie}")" || {
          EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve movie IMDb ID from TMDb ID '${tmdb_movie}'."
          return 1
        }
        IFS='|' read -r tmdb_movie imdb_movie _ <<< "${record}"
      fi
      ;;
    *)
      EchoE "${FUNCNAME[0]}:${LINENO}: Invalid link type '${type}'. Must be A, M, or C."
      return 1
      ;;
  esac

  # Ensure links database is initialized
  DBMovie_InitLinksDb || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to initialize links database."
    return 1
  }

  # Remove any existing identical entry
  grep -v -F "|${url}" "${file}" > "${file}.tmp" 2>/dev/null || true

  # Append the new record
  echo "${type}|${tmdb_actor}|${imdb_actor}|${tmdb_movie}|${imdb_movie}|${url}" >> "${file}.tmp"

  mv "${file}.tmp" "${file}"
  
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Saved link: Type=${type}, URL=${url}."
  return 0
}

#==============================================================================
# DBMovie_SortUniqueLinks
#
# Sort and remove duplicate entries inside links.db.
# Preserves the header, sorts the rest uniquely.
#
# Globals used:
#   - LINKS_DB_PATH
# Globals modified:
#   - links.db (rewritten in place)
#
# Arguments:
#   None
#
# Outputs:
#   - Overwrites links.db sorted and deduplicated
#   - Logs debug message if DEBUG is enabled
#
# Return:
#   0 - Always returns 0
#==============================================================================
DBMovie_SortUniqueLinks() {
  local -r file="${LINKS_DB_PATH}"

  # Always ensure database is initialized first
  DBMovie_InitLinksDb || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to initialize links database."
    return 1
  }

  local -r tmpfile="${file}.tmp"

  # Sort and deduplicate while preserving the header
  {
    head -n 1 "${file}"
    tail -n +2 "${file}" | sort -u
  } > "${tmpfile}"

  mv "${tmpfile}" "${file}"

  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: links.db sorted and deduplicated successfully."

  return 0
}

#-----------------------------------------------------------------------------#
#                                                                             |
#                                                                             |
#                                O T H E R                                    |
#                                                                             |
#                                                                             |
#-----------------------------------------------------------------------------#

#==============================================================================
# DBMovie_InitDb
#
# Initialize all local movie database files (actor.db, movie.db, links.db,
# movie_credits.db). Ensures they exist and have correct headers.
#
# Globals used:
#   - ACTOR_DB_PATH
#   - MOVIE_DB_PATH
#   - LINKS_DB_PATH
#   - MOVIE_CREDITS_DB_PATH
#
# Globals modified:
#   - Creates or updates all .db files as needed
#
# Arguments:
#   None
#
# Return:
#   0 - All databases initialized successfully
#   1 - Failure to initialize one or more databases
#==============================================================================
DBMovie_InitDb() {
  if DBMovie_InitActorDb \
    && DBMovie_InitMovieDb \
    && DBMovie_InitLinksDb \
    && DBMovie_InitMovieCreditsDb; then
    return 0
  else
    EchoE "${FUNCNAME[0]}:${LINENO}: Not possible to initialize the movie database."
    return 1
  fi
}

#***************************************************************************************
#***************************************************************************************
#***************************************************************************************
#***************************************************************************************

#==============================================================================
# HandleActorMode
#
# Handle operations related to actor-based post creation and metadata update.
#
# Globals used:
#   - FORCE_ACTOR_RESCAN
#   - DRY_RUN
#   - VERBOSE
#
# Globals modified:
#   - ACTOR_NAME
#   - ACTOR_TMDB_ID
#   - ACTOR_IMDB_ID
#
# Arguments:
#   ${1} - Actor name or IMDb ID
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
# HandleActorMode() {
#   local -r actor_input="$1"
# 
#   if [[ "${FORCE_ACTOR_RESCAN}" -eq 1 ]]; then
#     Echo "🔄 Force-rescanning actor '${actor_input}'..."
#     FetchActorListFromTmdb "${actor_input}" || {
#       EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch actor list."
#       return 1
#     }
#   else
#     ResolveActorIds "${actor_input}" || {
#       EchoE "${FUNCNAME[0]}:${LINENO}: Failed to resolve actor IDs."
#       return 1
#     }
#   fi
# 
#   DBMovie_LoadActorRecordParse "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" || {
#     EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch actor metadata."
#     return 1
#   }
# 
#   DBMovie_LoadMovieCreditByActorId "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}"  || {
#     EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch movie credits."
#     return 1
#   }
# 
#   GenerateActorBlogPost "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" || {
#     EchoE "${FUNCNAME[0]}:${LINENO}: Failed to generate blog post."
#     return 1
#   }
# 
#   return 0
# }

#==============================================================================
# HandleMovieMode
#
# Handle operations related to movie-based metadata fetching and blog post creation.
#
# Globals used:
#   - DRY_RUN
#   - VERBOSE
#
# Globals modified:
#   - None
#
# Arguments:
#   ${1} - Movie name or IMDb ID
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
# HandleMovieMode() {
#   local -r movie_input="$1"
# 
#   EchoE "${FUNCNAME[0]}:${LINENO}: Movie handling not implemented yet."
#   return 1
# }

#==============================================================================
# EnsureDependency: Ensure tool is available via install, fallback, or error
#------------------------------------------------------------------------------
# Description:
#   Attempts to locate a required CLI tool. If not found, attempts to install
#   it based on the current OS:
#     - Cygwin: runs local setup-x86_64.exe with -P <package>
#     - Debian/Ubuntu: uses apt-get
#     - macOS: uses brew
#
#   If install fails or is unsupported, falls back to a named function if given.
#
# Arguments:
#   $1 - Tool name (e.g. "jq")
#   $2 - Package name (same as tool or alternative)
#   $3 - Fallback function name (optional)
#
# Globals:
#   DEBUG (controls debug output)
# Outputs:
#   - Echo status messages
#   - Defines fallback function if install fails and fallback is provided
#==============================================================================
EnsureDependency() {
  local -r tool="${1}"
  local -r package="${2}"
  local -r fallback_func="${3:-}"

  if command -v "${tool}" &>/dev/null; then
    (( DEBUG )) && EchoD "[EnsureDependency] '${tool}' found"
    return 0
  fi

  EchoW "Missing tool: '${tool}'. Attempting to install package: ${package}..."

  case "$(uname -s)" in
    CYGWIN*)
      local installer="/setup-x86_64.exe"
      [[ -x "${installer}" ]] || installer="/cygdrive/f/Downloads/setup-x86_64_2.932.exe"
      if [[ -x "${installer}" ]]; then
        "${installer}" -q -P "${package}" &>/dev/null
      else
        EchoW "Cygwin installer not found. Please install '${package}' manually."
      fi
      ;;
    Linux)
      if command -v apt-get &>/dev/null; then
        sudo apt-get update && sudo apt-get install -y "${package}"
      fi
      ;;
    Darwin)
      if command -v brew &>/dev/null; then
        brew install "${package}"
      fi
      ;;
    *)
      EchoW "Unsupported platform. Cannot install '${package}' automatically."
      ;;
  esac

  if command -v "${tool}" &>/dev/null; then
    Echo "✔ Tool '${tool}' is now available."
    return 0
  elif [[ -n "${fallback_func}" ]]; then
    EchoW "Tool '${tool}' still missing. Attempting fallback: '${fallback_func}'"
    "${fallback_func}"
    return $?
  else
    ExitWithMsg "Required tool '${tool}' could not be installed."
  fi
}

#==============================================================================
# DefineUrlencodeFallback: Define a fallback implementation for 'urlencode'
#------------------------------------------------------------------------------
# Description:
#   Creates a shell function named `urlencode` to encode a string for safe use
#   in URLs if it is not already defined. This fallback is only used when a
#   native or installed `urlencode` command is unavailable.
#
#   Priority is given to the following implementations:
#     1. Python 3 (urllib.parse.quote)
#     2. Perl (URI::Escape)
#     3. Pure Bash fallback using percent encoding
#
#   This function should be called during dependency checks when
#   --check-deps-local or --check-deps-install is active.
#
# Globals Used:
#   DEBUG - If set, logs debug output on existing function
#
# Outputs:
#   - Defines a shell function named `urlencode`
#   - Logs which method was used for fallback definition
#
# Return:
#   0 - Always
#==============================================================================
DefineUrlencodeFallback() {
  if declare -F urlencode &>/dev/null; then
    (( DEBUG )) && EchoD "'urlencode' already defined, skipping fallback setup"

  elif command -v python3 &>/dev/null; then
    urlencode() {
      python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$1"
    }
    EchoW "Defined fallback 'urlencode' using python3"

  elif command -v perl &>/dev/null; then
    urlencode() {
      perl -MURI::Escape -e 'print uri_escape($ARGV[0]);' "$1"
    }
    EchoW "Defined fallback 'urlencode' using perl"

  else
    urlencode() {
      local LANG=C
      local string="${1}"
      local length="${#string}"
      local i
      for (( i = 0; i < length; i++ )); do
        local c="${string:$i:1}"
        case "${c}" in
          [a-zA-Z0-9.~_-]) printf "%s" "${c}" ;;
          *) printf "%%%02X" "'${c}" ;;
        esac
      done
    }
    EchoW "Defined fallback 'urlencode' using pure bash"
  fi

  return 0
}

#==============================================================================
# GetBlogAccessToken: Retrieve a Blogger API access token using a refresh token
#------------------------------------------------------------------------------
# Description:
#   Obtains a new access token from the Google OAuth 2.0 endpoint using the
#   configured refresh token. This function retries every few seconds until
#   a valid token is obtained and exported.
#
#   It uses the `curl` command to send a POST request and parses the JSON
#   response to extract `.access_token` using `jq`. If the token is invalid
#   or the request fails, the function sleeps for a fixed interval and retries.
#
# Globals Used:
#   GOOGLE_CLIENT_ID       - OAuth 2.0 client ID
#   GOOGLE_CLIENT_SECRET   - OAuth 2.0 client secret
#   GOOGLE_REFRESH_TOKEN   - Refresh token obtained via initial authorization
#   DEBUG                  - If set, logs additional diagnostics
#
# Globals Set:
#   BLOG_ACCESS_TOKEN      - The newly retrieved Blogger API access token (exported)
#
# Outputs:
#   - Echoes status messages and errors to the console
#   - Logs detailed retry messages if the request fails
#
# Return:
#   0 - Once a valid access token is retrieved
#==============================================================================
GetBlogAccessToken() {
  Echo "Obtaining Blog Access Token..."

  declare -g BLOG_ACCESS_TOKEN=""
  local -i attempt=0
  local -r MAX_WAIT=2

  while :; do
    (( attempt++ ))

    local BLOG_TOKEN_RESPONSE
    BLOG_TOKEN_RESPONSE=$(curl --silent --request POST \
      --data "client_id=${GOOGLE_CLIENT_ID}" \
      --data "client_secret=${GOOGLE_CLIENT_SECRET}" \
      --data "refresh_token=${GOOGLE_REFRESH_TOKEN}" \
      --data "grant_type=refresh_token" \
      https://oauth2.googleapis.com/token)

    BLOG_ACCESS_TOKEN=$(jq -r '.access_token' <<<"${BLOG_TOKEN_RESPONSE}")

    if [[ -n "${BLOG_ACCESS_TOKEN}" && "${BLOG_ACCESS_TOKEN}" != "null" ]]; then
      (( DEBUG )) && EchoD "Access token retrieved on attempt #${attempt}"
      break
    fi

    EchoE "Failed to retrieve a valid access token. Retrying in ${MAX_WAIT}s (attempt #${attempt})..."
    sleep ${MAX_WAIT}
  done

  (( DEBUG )) && EchoD "Token is '${BLOG_ACCESS_TOKEN}'"
  return 0
}

#==============================================================================
# ValidateTmdbKey: Check if the configured TMDb API key is valid
#------------------------------------------------------------------------------
# Description:
#   Sends a test request to the TMDb API to validate the API key configured
#   in the global variable `TMDB_API_KEY`. The request uses a known IMDb ID
#   (`tt0030062`) to perform a simple lookup.
#
#   If the response contains a `.status_code` field, this typically indicates
#   an error such as invalid API key or unauthorized access. In that case,
#   a warning is shown and `TMDB_KEY_VALID` is set to 0. Otherwise, the key
#   is assumed to be valid, and `TMDB_KEY_VALID` is set to 1.
#
# Globals Used:
#   TMDB_API_KEY     - The TMDb API key to be tested
#   DEBUG            - If set, logs confirmation of validity
#
# Globals Set:
#   TMDB_KEY_VALID   - 1 if key is valid, 0 if invalid
#
# Outputs:
#   - Echoes a warning if the API key appears to be invalid
#   - Logs validation status if DEBUG is enabled
#
# Return:
#   0 - Always
#==============================================================================
ValidateTmdbKey() {
  local response
  response=$(curl --silent --get "https://api.themoviedb.org/3/find/tt0030062" \
    --data-urlencode "api_key=${TMDB_API_KEY}" \
    --data-urlencode "external_source=imdb_id")

  if jq -e '.status_code' <<<"${response}" &>/dev/null; then
    Echo "TMDb API key appears invalid. Actor data will be skipped unless --force-actors is used."
    TMDB_KEY_VALID=0
  else
    (( DEBUG )) && EchoD "TMDb API key is valid."
    TMDB_KEY_VALID=1
  fi
}

#==============================================================================
# ShowVersion
#
# Display the script version information.
#
# Globals used:
#   - SCRIPT_VERSION
# Globals modified:
#   - None
#
# Arguments:
#   None
#
# Return:
#   0 - Always
#==============================================================================
ShowVersion() {
  echo "${0##*/} version ${SCRIPT_VERSION}"
}

#==============================================================================
# ShowHelp: Display detailed usage help for this script
#------------------------------------------------------------------------------
# Globals:
#   SCRIPT_VERSION      - Script version (used in --version output)
# Outputs:
#   Prints all supported options, modes, and examples to stdout
#==============================================================================
ShowHelp() {
  cat <<EOF

Usage: ${0##*/} [OPTIONS]

Modes:
  --actor NAME               Generate a filmography post for the specified actor
  --imdb ID                  Use IMDb ID directly instead of actor name (e.g., nm0000493)
  --movie-list               Generate an update post based on modified movie folders

Actor Cache Utility:
  --show-blogger-url         Print Blogger URL for actor in cache
  --delete-blogger-url       Delete Blogger URL for actor in cache
  --update-blogger-url URL   Update Blogger URL for actor in cache
  --revert-blogger-url [N]   Revert Blogger URL to previous state (default N=1)
  --history-blogger-url      Show full Blogger URL change history

  --show-photo-url           Print photo URL for actor in cache
  --delete-photo-url         Delete photo URL for actor in cache
  --update-photo-url URL     Update photo URL for actor in cache
  --revert-photo-url [N]     Revert photo URL to previous state (default N=1)
  --history-photo-url        Show full photo URL change history

Dependency Check Modes (mutually exclusive):
  --check-deps               Check for required tools; exit if any are missing
  --check-deps-local         Check and fallback to local functions when possible
  --check-deps-install       Attempt to install missing tools via Cygwin installer

General Options:
  -h, --help                 Show this help message and exit
  -V, --version              Show script version and exit
  -v, --verbose              Increase verbosity (can be repeated)
  -D, --debug                Enable debug output
  -n, --dry-run              Show what would happen, but make no changes
      --force                Override checks (e.g., update identical Blogger URL)

Movie Update Options (used with --movie-list):
  -d, --date DATE            Specify reference date (format: YYYY-MM-DD)
  -y, --year YEAR            Process only the given year
  -r, --range RANGE          Process a year range (e.g., 1930-1939 or -2020 or 1990-)
  -t, --title TITLE          Override default Blogger post title
  -l, --label LABEL          Add a Blogger label (can be repeated)
      --publish              Submit post as LIVE (default is DRAFT)
      --force                Skip confirmation warning when using --publish with year filters
      --no-html              Do not generate HTML in output
      --force-actors         Try to resolve actor info from TMDb even if key is invalid

Examples:
  ${0##*/} --actor "Jack Lemmon"
  ${0##*/} --imdb nm0000493
  ${0##*/} --actor "Jack Lemmon" --update-blogger-url 'https://...'
  ${0##*/} --actor "Jack Lemmon" --revert-url 2
  ${0##*/} --movie-list --date 2025-04-16 --range 1930-1939 --publish
  ${0##*/} --check-deps-local

EOF
}

#==============================================================================
# GetActorLifespan: Builds string "Name (YYYY–YYYY)" using parsed metadata
#------------------------------------------------------------------------------
# Description:
#   Constructs a human-readable actor lifespan string using global metadata
#   variables populated during actor data processing. The format follows:
#
#     Name (YYYY–YYYY)   ← if death year is known
#     Name (YYYY– )      ← if actor is still living or date is unknown
#
#   If either the birth or death year is not available, fallback values are
#   used ("????" for birth, empty string for death). This string is primarily
#   used in blog post titles and summaries.
#
# Globals Used:
#   ACTOR_NAME - Actor's full name
#   ACTOR_DOB  - Actor's birth date (YYYY-MM-DD or partial)
#   ACTOR_DOD  - Actor's date of death (optional)
#   DEBUG      - If set, prints diagnostics
#
# Outputs:
#   Echoes the formatted lifespan string to stdout
#
# Return:
#   0 - Always
#==============================================================================
GetActorLifespan() {
  EchoD "Inside GetActorLifespan: ACTOR_NAME=${ACTOR_NAME}, ACTOR_DOB=${ACTOR_DOB}, ACTOR_DOD=${ACTOR_DOD}"
  if [[ -z "${ACTOR_NAME}" ]]; then
    EchoW "GetActorLifespan called before ACTOR_NAME was populated!"
  fi

  local name="${ACTOR_NAME}"
  local birth="${ACTOR_DOB}"
  local death="${ACTOR_DOD}"

  # Format year of birth and death
  local born="${birth:0:4}"
  local died="${death:0:4}"

  # Fallback logic
  [[ -z "${born}" || "${born}" == "null" ]] && born="????"
  [[ -z "${died}" || "${died}" == "null" ]] && died=""

  # Compose final string
  if [[ -n "${died}" ]]; then
    echo "${name} (${born}–${died})"
  else
    echo "${name} (${born}– )"
  fi
}

#==============================================================================
# GenerateActorFilmography
#
# Generate a full filmography HTML block for an actor, using only local cache
# (actor.db, movie.db, movie_credits.db, links.db). No unnecessary online fetch.
#
# Arguments:
#   ${1} - Actor ID (either TMDb or IMDb ID)
#
# Globals Used:
#   - ACTOR_DB_PATH
#   - MOVIE_DB_PATH
#   - MOVIE_CREDITS_DB_PATH
#   - LINKS_DB_PATH
#   - TMDB_API_KEY
#   - DEBUG, VERBOSE
#
# Globals Set:
#   - ACTOR_NAME, ACTOR_DOB, ACTOR_DOD, ACTOR_PHOTO_URL
#   - ACTOR_TMDB_ID, ACTOR_IMDB_ID, ACTOR_BLOG_URL
#   - ACTOR_FILMOGRAPHY (HTML output)
#
# Return:
#   0 - Success
#   1 - Failure
#==============================================================================
GenerateActorFilmography() {
  local -r input_id="${1:-}"
  local record=""
  local movie_records=""
  local html=""
  local birth_year=""
  local -i age=0

  if [[ -z "${input_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Missing actor ID input."
    return 1
  fi

  # Load actor metadata from local cache
  if ! record=$(DBMovie_LoadActorRecord "${input_id}"); then
    EchoE "${FUNCNAME[0]}:${LINENO}: Actor ID '${input_id}' not found in cache."
    return 1
  fi

  IFS='|' read -r ACTOR_TMDB_ID ACTOR_IMDB_ID ACTOR_NAME ACTOR_DOB ACTOR_DOD ACTOR_PHOTO_URL ACTOR_BLOG_URL <<< "${record}"
  ACTOR_BIRTH_YEAR="${ACTOR_DOB:0:4}"

  EchoD "${FUNCNAME[0]}:${LINENO}: Loaded actor: ${ACTOR_NAME} (TMDb: ${ACTOR_TMDB_ID}, IMDb: ${ACTOR_IMDB_ID})"

  # Load associated movie records
  if ! movie_records=$(DBMovie_LoadMoviesByActor "${ACTOR_TMDB_ID}"); then
    EchoE "${FUNCNAME[0]}:${LINENO}: No movie records found for actor TMDb ID ${ACTOR_TMDB_ID}."
    return 1
  fi

  html+='<h2>Filmography: '"${ACTOR_NAME}"'</h2>'
  html+='<table border="1" style="font-size: small;">'
  html+='<tr style="background-color:#004080;color:white;font-weight:bold;">'
  html+='<th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th></tr>'

  while IFS='|' read -r tmdb_id imdb_id country title_en title_orig release_date watched_date blogger_url photo_url; do
    [[ -z "${tmdb_id}" || "${tmdb_id}" == "TMDB_ID" ]] && continue  # skip header or empty lines

    local role=""
    if ! role=$(DBMovie_GetMovieRole "${tmdb_id}" "${ACTOR_TMDB_ID}"); then
      role=""
    fi

    local safe_role="$(echo "${role}" | sed -E 's/&/&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')"
    local release_year="${release_date:0:4}"
    local imdb_link="${title_en}"

    # Link to IMDb if available
    if [[ -n "${imdb_id}" && "${imdb_id}" != "null" ]]; then
      imdb_link="<a href=\"https://www.imdb.com/title/${imdb_id}\" target=\"_blank\">${title_en}</a>"
    fi

    # Age calculation
    age="?"
    if [[ -n "${ACTOR_BIRTH_YEAR}" && "${release_year}" =~ ^[0-9]{4}$ ]]; then
      age=$(( release_year - ACTOR_BIRTH_YEAR ))
      if (( age < 0 || age > 110 )); then
        age="?"
      fi
    fi

    local watch_mark=""
    [[ -n "${watched_date}" ]] && watch_mark="&#x2705;"

    (( VERBOSE > 1 )) && Echo "        Movie: '${title_en}'  Role: '${role}'  Year: '${release_year}'  Age: '${age}'"

    html+="<tr><td>${release_date}</td><td>${imdb_link}</td><td>${safe_role}</td><td>${age}</td><td>${watch_mark}</td></tr>"
  done <<< "${movie_records}"

  html+="</table>"

  export ACTOR_FILMOGRAPHY="${html}"
  return 0
}

#==============================================================================
# ResolveActorImdbId: Resolves actor name to IMDb ID via TMDb
#------------------------------------------------------------------------------
# Description:
#   This function performs a two-step resolution:
#
#     1. Uses the actor name to retrieve the corresponding TMDb person ID
#        (via `GetActorTmdbId`, which queries TMDb).
#     2. Uses that TMDb ID to fetch external identifiers and extract the
#        associated IMDb ID using the /external_ids endpoint.
#
#   If any step fails, an error is printed and the function returns 1.
#
# Arguments:
#   ${1} - Actor name (e.g., "Jack Lemmon")
#
# Globals Used:
#   TMDB_API_KEY - The TMDb API key required for both queries
#
# Outputs:
#   - Echoes the resolved IMDb ID (e.g., nm0000493) to stdout
#   - Prints error messages via EchoE on failure
#
# Return:
#   0 - IMDb ID was successfully resolved
#   1 - Any error during TMDb resolution or API failure
#==============================================================================
ResolveActorImdbId() {
  EchoD "${FUNCNAME[0]}:${LINENO}: $*"
  local -r actor_name="${1}"
  
  GetActorTmdbId "${actor_name}" || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Could not find TMDb ID for: ${actor_name}"
    exit 1
  }
  local tmdb_id="${ACTOR_TMDB_ID_RESULT}"

  if [[ "${tmdb_id}" == "${MANY_RECORD_MARK}" ]]; then
    local -r result_json="${_ACTOR_SEARCH_RESULT_JSON}"
    local -i index=0
    Echo "Multiple matches found for '${actor_name}'. Loading additional information."
    Echo
    # Print header
    Echo "Idx  TMDB_ID  Gender   Name                    Depto           Birth       Death" >&${CALL_STDOUT_FD}
    Echo "---  -------  -------  ----------------------  --------------  ----------  ----------" >&${CALL_STDOUT_FD}
    local -i max_options=-1
    local -a asOptions
    while IFS='|' read -r idx id gender name dept; do
      (( max_options++ ))
      # Fetch additional metadata: birthday and deathday
      local person_json=$(curl --silent --fail "https://api.themoviedb.org/3/person/${id}?api_key=${TMDB_API_KEY}")
      asOptions[${max_options}]="$(printf "%-4s %-8s %-8s %-23s %-15s %-11s %-10s\n" \
          "${idx}"    \
          "${id}"     \
          "${gender}" \
          "${name}"   \
          "${dept}"   \
          $(jq -r '.birthday // "N/A"' <<< "${person_json}") \
          $(jq -r '.deathday // "N/A"' <<< "${person_json}") )"
      Echo "${asOptions[${max_options}]}"
    done < <(jq -r '
      .results
      | to_entries[]
      | "\(.key)|\(.value.id)|\(
          if .value.gender == 1 then "Female"
          elif .value.gender == 2 then "Male"
          else "Unknown"
        end)|\(.value.name)|\(.value.known_for_department)"
    ' <<< "${result_json}"
    )
    Echo
    tmdb_id=
    while true; do
      EchoN "Select the correct TMDb from 0 to ${max_options} or 'q' to quit: "
      read -u ${CALL_STDIN_FD} -r selection
      [[ ${selection} -ge 0 && ${selection} -le ${max_options} ]] && {
        tmdb_id="$(echo "${asOptions[${selection}]}"|awk '{print $2}')"; break; 
      }
      selection="${selection:0:1}"
      [[ "${selection,,}" == 'q' ]] && { Echo "Aborted by user."; exit 1 ; }
    done
    Echo "Using TMDB id ${tmdb_id}"
  elif [[ -z "${tmdb_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Could not resolve TMDb ID for: ${actor_name}"
    exit 1
  fi
  # local -r imdb_id=$(curl --silent --fail "https://api.themoviedb.org/3/person/${tmdb_id}/external_ids?api_key=${TMDB_API_KEY}" | jq -r '.imdb_id // empty')
  # [[ -z "${imdb_id}" ]] && {
  #   EchoE "Failed to resolve IMDb ID from TMDb ID ${tmdb_id}."
  #   exit 1
  # }
  DBMovie_LoadActorRecordParse "${tmdb_id}" >/dev/null || {
    EchoE "${FUNCNAME[0]}:${LINENO}: Could not fetch actor metadata for IMDb ID: ${tmdb_id}"
    return 1
  }

  echo "${ACTOR_IMDB_ID}"
  return 0
}

#==============================================================================
# GetTopActorsFromTmdb: Generate collapsible HTML block with top 5 actors
#------------------------------------------------------------------------------
# Description:
#   Given an IMDb movie ID and its release year, this function:
#
#     1. Resolves the corresponding TMDb movie ID.
#     2. Fetches the top cast members using the /credits endpoint.
#     3. For each of the top 5 cast members:
#        - Fetches actor metadata (birth year)
#        - Calculates their age at the time of release
#        - Builds a row in an HTML <table> with actor name, role, and age
#
#   The output is a collapsible <details> HTML section suitable for Blogger.
#   It includes clickable TMDb profile links for each actor.
#
# Arguments:
#   ${1} - IMDb ID of the movie (e.g., tt0030062)
#   ${2} - Year of movie release (used for age calculation)
#
# Globals Used:
#   TMDB_API_KEY - API key for TMDb
#   VERBOSE      - If >1, prints actor details to the screen for debugging
#   DEBUG        - Enables debug output for resolution failures
#
# Outputs:
#   Echoes an HTML string block (<details> + <table>) to stdout
#
# Return:
#   0 - Success (even if partial or limited results)
#   1 - TMDb ID resolution failure or missing actor data
#==============================================================================
GetTopActorsFromTmdb() {
  local -r imdb_id="${1}"
  local -r release_year="${2}"
  local tmdb_id
  tmdb_id=$(curl --silent "https://api.themoviedb.org/3/find/${imdb_id}?api_key=${TMDB_API_KEY}&external_source=imdb_id" | jq -r '.movie_results[0].id')

  if [[ -z "${tmdb_id}" || "${tmdb_id}" == "null" ]]; then
    (( DEBUG )) && Echo "${FUNCNAME[0]}:${LINENO}: No TMDb ID found for IMDb: ${imdb_id}"
    return
  fi

  local cast_json
  cast_json=$(curl --silent "https://api.themoviedb.org/3/movie/${tmdb_id}/credits?api_key=${TMDB_API_KEY}")

  local html='<details style="margin-top:8px;"><summary style="cursor:pointer;font-weight:bold;">[+]&nbsp; Top Cast</summary>'
  html+='<table border="1" style="font-size: small; margin-top:5px;"><tr><th>Actor</th><th>Role</th><th>Age</th></tr>'

  for i in {0..5}; do
    local actor_id name role birthdate age

    actor_id=$(jq -r ".cast[${i}].id" <<<"${cast_json}")
    name=$(jq -r ".cast[${i}].name" <<<"${cast_json}")
    role=$(jq -r ".cast[${i}].character" <<<"${cast_json}")

    [[ -z "${actor_id}" || "${actor_id}" == "null" ]] && continue

    birthdate=$(curl --silent "https://api.themoviedb.org/3/person/${actor_id}?api_key=${TMDB_API_KEY}" | jq -r '.birthday')

    if [[ "${birthdate}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
      local birth_year="${birthdate:0:4}"
      age=$(( year - birth_year ))
    else
      age="?"
    fi

    # Build HTML with hyperlink
    local actor_link="<a href=\"https://www.themoviedb.org/person/${actor_id}\" target=\"_blank\">${name}</a>"

    (( VERBOSE > 1 )) && Echo "        Actor: '${name}'  Role: '${role}'  Age: '${age}'"

    html+="<tr><td>${actor_link}</td><td>${role}</td><td>${age}</td></tr>"
  done

  html+='</table></details>'
  echo "${html}"
}

#==============================================================================
# GetTitleFromOmdb: Resolves proper English title and metadata using OMDb API
#------------------------------------------------------------------------------
# Description:
#   Queries the OMDb API to fetch accurate metadata for a movie given its title
#   and expected release year. Results are cached in-memory using a global
#   associative array (METADATA_CACHE) to avoid redundant lookups.
#
#   Fields retrieved include:
#     - Proper English title
#     - Genre(s)
#     - IMDb ID
#     - Runtime
#     - Poster URL
#
#   If the API response year does not match the expected year, a warning is
#   issued and a fallback record is returned with the raw query string and "N/A"
#   placeholders.
#
# Arguments:
#   ${1} - Search query (movie title, as string)
#   ${2} - Expected release year (YYYY)
#
# Globals Used:
#   OMDB_API_KEY     - OMDb API key
#   DEBUG            - Enables debug output
#   METADATA_CACHE[] - Associative array for in-memory caching
#
# Outputs:
#   Echoes pipe-separated result string:
#     Title|||Genre|||IMDbID|||Runtime|||Poster
#
# Return:
#   0 - Success (valid result or fallback returned)
#   1 - None (all control handled internally; no external exit code)
#==============================================================================
GetTitleFromOmdb() {
  local -r query="${1}"
  local -r expected_year="${2}"
  local -r api_url="http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&t=$(urlencode "${query}")&y=${expected_year}&type=movie"

  # Check if already cached
  local cache_key="${query}_${expected_year}"
  if [[ -n "${METADATA_CACHE["${cache_key}"]}" ]]; then
    echo "${METADATA_CACHE["${cache_key}"]}"
    return
  fi

  local json
  json=$(curl --silent --max-time 10 "${api_url}")

  local success
  success=$(jq -r '.Response' <<< "${json}")
  local actual_year
  actual_year=$(jq -r '.Year // empty' <<< "${json}")

  # Check for success and valid year
  if [[ "${success}" == "True" && "${actual_year}" == "${expected_year}" ]]; then
    local proper_title=$(jq -r '.Title // empty' <<< "${json}")
    local genre=$(jq -r '.Genre // empty' <<< "${json}")
    local imdbid=$(jq -r '.imdbID // empty' <<< "${json}")
    local runtime=$(jq -r '.Runtime // empty' <<< "${json}")
    local poster=$(jq -r '.Poster // empty' <<< "${json}")
    local result="${proper_title}|||${genre}|||${imdbid}|||${runtime}|||${poster}"
    METADATA_CACHE["${cache_key}"]="${result}"
    (( DEBUG )) && EchoD "✅ OMDb title resolved: ${proper_title} [${genre}]"
    echo "${result}"
  else
    EchoW "OMDb lookup mismatch or failure: Wanted ${expected_year}, got '${actual_year}' for '${query}'"
    local fallback="${query}|||N/A|||N/A|||N/A|||N/A"
    METADATA_CACHE["${cache_key}"]="${fallback}"
    echo "${fallback}"
  fi
}

#==============================================================================
# GetActorPhotoUrl: Returns TMDb profile image URL for an actor
#------------------------------------------------------------------------------
# Description:
#   Given an IMDb actor ID stored in ACTOR_IMDB_ID, this function queries TMDb's
#   "find" endpoint to retrieve the actor's profile image path. If a profile image
#   exists, it returns the full TMDb URL to a 300px-wide version of the image.
#
# Globals Used:
#   ACTOR_IMDB_ID     - IMDb ID of the actor (e.g., nm0000493)
#   TMDB_API_KEY      - TMDb API key for querying person metadata
#
# Outputs:
#   Echoes the full profile image URL (https://image.tmdb.org/...) or empty string
#
# Return:
#   0 - Success (URL echoed or empty string)
#   1 - Function will not exit with error; result is determined by output
#==============================================================================
GetActorPhotoUrl() {
  local -r tmdb_find_url="https://api.themoviedb.org/3/find/${ACTOR_IMDB_ID}?api_key=${TMDB_API_KEY}&external_source=imdb_id"
  local profile_path

  profile_path=$(curl --silent --fail "${tmdb_find_url}" | jq -r '.person_results[0].profile_path')

  if [[ -n "${profile_path}" && "${profile_path}" != "null" ]]; then
    echo "https://image.tmdb.org/t/p/w300${profile_path}"
  else
    echo ""
  fi
}

#==============================================================================
# SubmitBlogDraft: Submits or updates a Blogger DRAFT post with HTML content
#------------------------------------------------------------------------------
# Description:
#   Sends a draft post to the Blogger API using OAuth authentication.
#   If a path is provided, attempts to update an existing post; otherwise,
#   creates a new draft. In DRYRUN mode, outputs intended payload without posting.
#
# Globals:
#   BLOG_ACCESS_TOKEN - OAuth 2.0 Bearer token for Blogger API
#   BLOG_ID           - Blogger blog ID to submit the post to
#   CUSTOM_TITLE      - Title of the blog post (used if not overridden)
#   DRYRUN            - If set to 1, skips API call and shows simulated output
#   DEBUG             - If set, emits debug logs using EchoD
#
# Arguments:
#   ${1} - HTML body content (unescaped raw HTML)
#   ${2} - Optional post path (e.g., "2025/04/some-title.html") to update
#
# Outputs:
#   On success, echoes the full Blogger post URL (https://...)
#   On failure, prints error message and returns non-zero exit status
#==============================================================================
SubmitBlogDraft() {
  local -r html_content="${1}"
  local -r post_path="${2:-}"
  local -r blog_title="${CUSTOM_TITLE:-Actor - ${ACTOR_QUERY} Filmography}"

  (( DEBUG )) && EchoD "${FUNCNAME[0]}| Title=${blog_title}"
  (( DEBUG )) && EchoD "${FUNCNAME[0]}| Update=${post_path:-<new>}"

  if (( DRYRUN )); then
    Echo "[DRY-RUN] Blog post would be submitted with title: ${blog_title}"
    Echo "[DRY-RUN] HTML Content:"
    Echo "${html_content}"
    return 0
  fi

  local -r payload="$(
    jq -n \
      --arg title "${blog_title}" \
      --arg content "${html_content}" \
      --arg blog_id "${BLOG_ID}" \
      '{
        kind: "blogger#post",
        blog: { id: $blog_id },
        title: $title,
        content: $content
      }'
  )"

  local response
  local url

  GetBlogAccessToken || {
    EchoE "Failed to retrieve Blogger API access token."
    return 1
  }
  if [[ -n "${post_path}" ]]; then
    response="$(curl --silent --fail --show-error --request PUT \
      --header "Authorization: Bearer ${BLOG_ACCESS_TOKEN}" \
      --header "Content-Type: application/json" \
      --data "${payload}" \
      "https://www.googleapis.com/blogger/v3/blogs/${BLOG_ID}/posts/bypath?path=${post_path}")"

    url="$(jq -r '.url // empty' <<< "${response}")"

    if [[ -z "${url}" ]]; then
      EchoE "Blogger update failed. Response: ${response}"
      return 1
    fi

    echo "${url}"
  else
    response="$(curl --silent --fail --show-error --request POST \
      --header "Authorization: Bearer ${BLOG_ACCESS_TOKEN}" \
      --header "Content-Type: application/json" \
      --data "${payload}" \
      "https://www.googleapis.com/blogger/v3/blogs/${BLOG_ID}/posts/")"

    url="$(jq -r '.url // empty' <<< "${response}")"

    if [[ -z "${url}" ]]; then
      EchoE "Blogger submission failed. Response: ${response}"
      return 1
    fi

    echo "${url}"
  fi
}

#==============================================================================
# BuildHtmlTable: Constructs the grouped HTML table from collected movie data
#------------------------------------------------------------------------------
# Description:
#   Uses the associative array `year_map` (year → HTML row list) to build a
#   full HTML <table> with one row per year and a bullet list of updated or
#   added movie titles. Sorting is done alphabetically within each year using
#   ASCII-safe, case-insensitive sorting (LC_ALL=C).
#
# Globals Used:
#   REFERENCE_DATE  - Date string used in header intro
#   year_map        - Associative array: [year] → list of raw HTML lines
#
# Outputs:
#   Echoes full HTML block to stdout
#
# Return:
#   0 - Success
#==============================================================================
BuildHtmlTable() {
  local -r intro="These are the movie titles added (new titles) or updated (new format for an existing title) on ${REFERENCE_DATE}:<br><br>"
  local html='<!-- Generated by MovieUpdateDraft.sh -->'
  html+='<table border="1">'
  html+='<tr style="background-color:#004080;color:white;font-weight:bold;"><th>Year</th><th>Updated Titles</th></tr>'

  for year in $(printf "%s\n" "${!year_map[@]}" | sort); do
    local raw_entries
    IFS=$'\n' read -rd '' -a raw_entries < <(printf "%s\n" "${year_map[$year]}" | grep -v '^\s*$' && printf '\0')

    # Build sortable pairs: TITLE|||HTML_LINE
    local -a sortable_lines=()
    for line in "${raw_entries[@]}"; do
      line=$(echo "${line}" | tr -d '\r\n')

      # Try to extract title from anchor tag
      local display_text
      if [[ "${line}" =~ $'^<a [^>]*>([^<]+)</a>' ]]; then
        display_text="${BASH_REMATCH[1]}"
      else
        # Fallback: use first few words from line
        display_text=$(echo "${line}" | sed -E 's/^([^<]+) .*/\1/')
      fi

      # Remove any multibyte/UTF-8 characters just for sorting
      display_text=$(echo "${display_text}" | iconv -c -f UTF-8 -t ASCII//TRANSLIT)

      sortable_lines+=( "${display_text}|||${line}" )
    done

    # Sort by display title (case-insensitive, ASCII-safe)
    local -a sorted_lines=()
    IFS=$'\n' read -rd '' -a sorted_lines < <(
      printf "%s\n" "${sortable_lines[@]}" | LC_ALL=C sort -f | sed 's/^[^|]*|||//' && printf '\0'
    )

    html+="<tr><td>${year}</td><td><ul>"
    for sorted_line in "${sorted_lines[@]}"; do
      html+="<li>${sorted_line}</li>"
    done
    html+="</ul></td></tr>"
  done

  html+="</table>"
  printf "%s" "${intro}${html}"
}

#==============================================================================
# BuildSummaryHtml: Generate a visual summary block for updated movie entries
#------------------------------------------------------------------------------
# Description:
#   Constructs an HTML-formatted summary of changes made for the reference
#   date. It includes total counts (years, titles, added/updated) and a
#   breakdown by year. Uses emojis and bold formatting for clarity.
#
# Globals Used:
#   year_map            - Associative array of year → titles
#   year_added_count    - Associative array of year → count of added titles
#   year_changed_count  - Associative array of year → count of updated titles
#   total_added         - Total number of newly added titles
#   total_changed       - Total number of updated titles
#
# Outputs:
#   Echoes a full HTML <ul>-formatted block with summary data to stdout
#
# Return:
#   0 - Success
#==============================================================================
BuildSummaryHtml() {
  local -i total_years=${#year_map[@]}
  local -i total_titles=$((total_added + total_changed))

  local html='<br><br><b>Summary:</b><br>'
  html+='<ul>'
  html+="<li>🗓️ Total Years Updated: <b>${total_years}</b></li>"
  html+="<li>🎬 Total Titles: <b>${total_titles}</b></li>"
  html+="<li>🆕 New Titles: <b>${total_added}</b></li>"
  html+="<li>♻️ Updated Titles: <b>${total_changed}</b></li>"
  html+='</ul>'

  html+='<b>Details per Year:</b><br>'
  html+='<ul>'
  for year in $(printf "%s\n" "${!year_map[@]}" | sort); do
    local -i added=${year_added_count["${year}"]:-0}
    local -i changed=${year_changed_count["${year}"]:-0}
    html+="<li><b>${year}</b>: 🆕 ${added}, ♻️ ${changed}</li>"
  done
  html+='</ul>'

  printf "%s" "${html}"
}

#==============================================================================
# ScanDirectories: Traverse movie directories and detect updates by date
#------------------------------------------------------------------------------
# Description:
#   Walks through all year-based subdirectories under ${MOVIE_ROOT}, identifying
#   directories that have been modified or added based on file timestamps
#   between REFERENCE_DATE_EPOCH and TODAY_EPOCH. For each qualifying folder:
#   - Determines if it is a new title or an update
#   - Retrieves OMDb metadata (title, genre, runtime, IMDb ID)
#   - Optionally fetches top actors from TMDb (if enabled)
#   - Formats HTML blocks with poster, cast, and metadata
#
#   Updates year_map[], year_added_count[], year_changed_count[],
#   total_added and total_changed globals accordingly.
#
# Globals Used:
#   MOVIE_ROOT            - Root path to year-based movie folders
#   REFERENCE_DATE        - Target date in YYYY-MM-DD format
#   REFERENCE_DATE_EPOCH  - Epoch of reference date
#   TODAY_EPOCH           - Epoch of today’s date
#   VERBOSE               - Controls verbosity of output
#   DEBUG                 - Enables debug logging
#   FILTER_YEARS[]        - Optional filter list of years to include
#   FORCE_ACTORS          - If set, actor info is always fetched
#   TMDB_KEY_VALID        - TMDb key validation result
#   year_map[]            - Associative array for HTML generation
#   year_added_count[]    - Count of added titles per year
#   year_changed_count[]  - Count of updated titles per year
#   total_added           - Total count of new titles
#   total_changed         - Total count of changed titles
#
# Outputs:
#   Populates year_map with resolved HTML snippets per year
#   Updates count globals for summary reporting
#
# Return:
#   0 - Always
#==============================================================================
ScanDirectories() {
  Echo "Scanning directories for updates on ${REFERENCE_DATE}..."

  # Stats tracking
  declare -gA year_added_count=()
  declare -gA year_changed_count=()
  declare -gi total_added=0
  declare -gi total_changed=0

  for year_dir in "${MOVIE_ROOT}"/*; do
    [[ -d "${year_dir}" ]] || continue
    [[ "${year_dir##*/}" =~ ^! ]] && continue
    local year="${year_dir##*/}"
    [[ "${year}" =~ ^[0-9]{4}$ ]] || continue

    # Apply year filter if provided
    if [[ ${#FILTER_YEARS[@]} -gt 0 ]]; then
      local -i match=0
      for filter in "${FILTER_YEARS[@]}"; do
        [[ "${year}" == "${filter}" ]] && match=1 && break
      done
      (( match == 0 )) && continue
    fi

    Echo "Processing year: ${year}"

    while IFS= read -r -d '' title_dir; do
      [[ "${title_dir##*/}" =~ ^! ]] && continue

      local folder_name="${title_dir##*/}"
      (( DEBUG )) && [[ -z "${folder_name}" ]] && EchoD "⚠️ Empty folder name extracted from: ${title_dir}"
      [[ -z "${folder_name}" ]] && continue

      (( DEBUG )) && EchoD "  Checking title: ${folder_name}"
      local -i matched=0
      local -i is_added=1

      while IFS= read -r -d '' file; do
        local target
        target=$(readlink -f "${file}")
        [[ -z "${target}" ]] && continue
        local -i file_epoch
        file_epoch=$(stat -c %Y "${target}")

        (( DEBUG )) && EchoD "    File: ${file} -> Target: ${target} (Epoch: ${file_epoch})"

        if (( file_epoch >= REFERENCE_DATE_EPOCH && file_epoch < TODAY_EPOCH )); then
          [[ -e "${title_dir}/metadata.json" ]] && is_added=0
          local update_type
          update_type=$([[ ${is_added} -eq 1 ]] && echo "Added" || echo "Changed")

          # Count BEFORE OMDb (even if OMDb fails)
          if (( is_added )); then
            (( year_added_count["${year}"]++ ))
            (( total_added++ ))
          else
            (( year_changed_count["${year}"]++ ))
            (( total_changed++ ))
          fi

          # OMDb call
          local resolved_full
          resolved_full=$(GetTitleFromOmdb "${folder_name}" "${year}")

          local resolved_title="${resolved_full%%|||*}"
          resolved_title="$(echo "${resolved_title}" | xargs)"  # <-- Trim after assigning

          EchoD "resolved_title='${resolved_title}' from resolved_full='${resolved_full}'"

          if [[ -z "${resolved_title}" || "${resolved_title}" == "null" ]]; then
            resolved_title="${folder_name}"
            (( DEBUG )) && EchoD "Fallback: using folder name as title: '${resolved_title}'"
          fi
          
          # Verbose output of matched title
          if (( VERBOSE == 1 )); then
            Echo "    ${resolved_title} (updated ${REFERENCE_DATE})"
          elif (( VERBOSE > 1 )); then
            Echo "    -> MATCHED: ${resolved_title} (updated ${REFERENCE_DATE})"
          fi

          local temp_rest="${resolved_full#*|||}"
          local resolved_genre="${temp_rest%%|||*}"
          temp_rest="${temp_rest#*|||}"
          local resolved_imdbid="${temp_rest%%|||*}"
          temp_rest="${temp_rest#*|||}"
          local resolved_runtime="${temp_rest%%|||*}"
          local resolved_poster="${temp_rest#*|||}"

          (( DEBUG )) && EchoD "Calling TMDb with IMDb ID: ${resolved_imdbid}"
          if (( FORCE_ACTORS )) || (( TMDB_KEY_VALID )); then
            cast_html="$(GetTopActorsFromTmdb "${resolved_imdbid}" "${year}")"
            #EchoD "='${cast_html}'" 
          fi

          local poster_html=""

          # Start table if either poster or cast exists
          if [[ -n "${resolved_poster}" && "${resolved_poster}" != "N/A" || -n "${cast_html}" ]]; then
            poster_html='<table><tr>'

            # Poster column
            if [[ -n "${resolved_poster}" && "${resolved_poster}" != "N/A" ]]; then
              poster_html+='<td style="vertical-align:top;">'
              poster_html+='<img src="'"${resolved_poster}"'" width="120">'
              poster_html+='</td>'
            fi

            # Cast column
            if [[ -n "${cast_html}" ]]; then
              poster_html+='<td style="padding-left:8px; vertical-align:top;">'
              poster_html+="${cast_html}"
              poster_html+='</td>'
            fi

            poster_html+='</tr></table>'
          fi

          # Formatting
          local genre_suffix=""
          if [[ -n "${resolved_genre}" && "${resolved_genre}" != "N/A" ]]; then
            genre_suffix=" <span style=\"color:#800080;font-weight:bold;\">[${resolved_genre}, ${resolved_runtime}]</span>"
          fi

          local linked_title="${resolved_title}"
          if [[ -n "${resolved_imdbid}" && "${resolved_imdbid}" != "N/A" ]]; then
            linked_title="<a href=\"https://www.imdb.com/title/${resolved_imdbid}\" target=\"_blank\">${resolved_title}</a>"
          fi

          (( DEBUG )) && EchoD "✔ Counted '${resolved_title}' as ${update_type} for year ${year}"

          year_map["${year}"]+="${linked_title}${genre_suffix} (${update_type})${poster_html}"$'\n'
          matched=1
          break
        fi
      done < <(find "${title_dir}" -type l -print0)

      (( matched == 0 && VERBOSE > 1 )) && Echo "    -> No file matched."
    done < <(find "${year_dir}" -mindepth 1 -maxdepth 1 -type d -print0)
  done
}

#==============================================================================
# ParseArguments: Parse all command-line arguments and populate global variables.
#------------------------------------------------------------------------------
# Description:
#   Parses CLI flags to determine execution mode (e.g., --actor or --movie-list),
#   along with filters, toggles, and post behavior. Sets global control variables.
#
# Globals Set:
#   EXECUTION_MODE, EXECUTION_SUBMODE, ACTOR_QUERY, IMDB_ID,
#   DRYRUN, VERBOSE, DEBUG, FORCE, FORCE_ACTORS, FORCE_PUBLISH,
#   CUSTOM_DATE, CUSTOM_TITLE, LABELS[], FILTER_YEARS[],
#   CHECK_DEPS_DEFAULT, CHECK_DEPS_LOCAL, CHECK_DEPS_INSTALL,
#   VERBOSE_DEPS, USE_HTML, UPDATE_MODE, NEW_BLOGGER_URL, REVERT_STEPS
#
# Outputs:
#   - Populates globals based on flags
#   - Echoes errors and exits on invalid combinations
#==============================================================================
ParseArguments() {
  local -i nActorSwitchUsed=0
  local -i nMovieListUsed=0
  NEW_BLOGGER_URL=

  while [[ $# -gt 0 ]]; do
    case ${1} in
      -h|--help)    ShowHelp; exit 0 ;;
      -V|--version) ShowVersion; exit 0 ;;
      -n|--dry-run) DRYRUN=1 ; shift ;;
      -v|--verbose) ((VERBOSE++)) ; shift ;;
      -D|--debug)   ((DEBUG++)) ; shift ;;

      --check-deps) CHECK_DEPS_DEFAULT=1 ; shift ;;
      --check-deps-local) CHECK_DEPS_LOCAL=1 ; shift ;;
      --check-deps-install) CHECK_DEPS_INSTALL=1 ; shift ;;

      #-------------------------------------- ACTOR MODE
      --actor)
        shift
        if [[ -z "$1" ]]; then
          EchoE "--actor requires a name (e.g., --actor 'Jack Lemmon')"
          exit 1
        fi
        ACTOR_QUERY="$1"
        EXECUTION_MODE=2
        EXECUTION_SUBMODE=0
        nActorSwitchUsed=1
        shift
        ;;
      --force) FORCE=1 ; shift ;;
      --tmdb)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        shift
        if [[ -z "$1" || ! "$1" =~ ^[0-9]{3,}$ ]]; then
          EchoE "--tmdb requires a valid TMDb ID (e.g., 4392)"
          exit 1
        fi
        ACTOR_TMDB_ID="$1"
        shift
        ;;
      --imdb)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        shift
        if [[ -z "$1" || ! "$1" =~ ^nm[0-9]{7,}$ ]]; then
          EchoE "--imdb requires a valid IMDb ID (e.g., nm0000493)"
          exit 1
        fi
        ACTOR_IMDB_ID="$1"
        shift
        ;;
      --show-blogger-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        EXECUTION_SUBMODE=1
        shift
        ;;
      --delete-blogger-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        EXECUTION_SUBMODE=2
        shift
        ;;
      --update-blogger-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        shift
        if [[ -z "$1" ]]; then
          EchoE "--update-blogger-url requires a URL value"
          exit 1
        fi
        EXECUTION_SUBMODE=3
        NEW_BLOGGER_URL="$1"
        (( DEBUG )) && EchoD "NEW_BLOGGER_URL='${NEW_BLOGGER_URL}'"
        shift
        ;;
      --revert-blogger-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        REVERT_STEPS="${2:-1}"
        EXECUTION_SUBMODE=4
        shift 2
        ;;
      --history-blogger-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        EXECUTION_SUBMODE=5
        shift
        ;;

      --show-photo-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        EXECUTION_SUBMODE=6
        shift
        ;;
      --delete-photo-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        EXECUTION_SUBMODE=7
        shift
        ;;
      --update-photo-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        shift
        if [[ -z "$1" ]]; then
          EchoE "--update-photo-url requires a URL value"
          exit 1
        fi
        EXECUTION_SUBMODE=8
        NEW_PHOTO_URL="$1"
        (( DEBUG )) && EchoD "NEW_PHOTO_URL='${NEW_PHOTO_URL}'"
        shift
        ;;
      --revert-photo-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        REVERT_STEPS="${2:-1}"
        EXECUTION_SUBMODE=9
        shift 2
        ;;
      --history-photo-url)
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        EXECUTION_SUBMODE=10
        shift
        ;;

      --force-bio) 
        (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
        FORCE_BIO=1
        shift ;;
      #-------------------------------------- MOVIE LIST MODE
      --movie-list)
        if (( EXECUTION_MODE != 0 )); then
          EchoE "--movie-list cannot be used with another execution mode."
          exit 1
        fi
        EXECUTION_MODE=1
        nMovieListUsed=1
        shift
        ;;
      --update)
        shift
        UPDATE_MODE=1
        if [[ -n "${1}" && "${1}" != --* ]]; then
          ACTOR_UPDATE_PATH="${1}"
          shift
        fi
        ;;
      -d|--date)    CUSTOM_DATE="${2}" ; shift 2 ;;
      --force-actors) FORCE_ACTORS=1 ; shift ;;
      --no-html) USE_HTML=0 ; shift ;;
      --publish) FORCE_PUBLISH=1 ; shift ;;

      -t|--title) shift; CUSTOM_TITLE="$1"; shift ;;
      -l|--label) shift; LABELS+=("$1"); shift ;;

      -y|--year)
        [[ "$2" =~ ^[0-9]{4}$ ]] || { EchoE "Invalid year format: $2" ; exit 1; }
        FILTER_YEARS+=("$2")
        shift 2
        ;;
      -r|--range)
        shift
        if [[ "$1" =~ ^([0-9]{4})-([0-9]{4})$ ]]; then
          start=${BASH_REMATCH[1]}
          end=${BASH_REMATCH[2]}
        elif [[ "$1" =~ ^([0-9]{4})-$ ]]; then
          start=${BASH_REMATCH[1]}
          end=$(( start + 9 ))
        elif [[ "$1" =~ ^-([0-9]{4})$ ]]; then
          end=${BASH_REMATCH[1]}
          start=$(( end - (end % 10) ))
        else
          EchoE "Invalid range format: $1"
          exit 1
        fi
        if (( start > end )); then
          EchoE "Invalid range: ${start}-${end}"
          exit 1
        fi
        for (( y=start; y<=end; y++ )); do
          FILTER_YEARS+=("${y}")
        done
        shift
        ;;
      --) shift ; break ;;
      -*) EchoE "Unknown option: ${1}" ; PrintHelp ; exit 1 ;;
      *) POSITIONAL_ARGS+=("${1}") ; shift ;;
    esac
  done

  (( nMovieListUsed==1 && nActorSwitchUsed==1 )) && { EchoE "--movie-list cannot be combined with --actor"; exit 1; }
  (( nMovieListUsed==0 && nActorSwitchUsed==0 )) && { EchoE "No valid mode specified. Use --movie-list or --actor"; exit 1; }

  if (( nActorSwitchUsed==1 )); then
    if [[ -n "${ACTOR_QUERY}" && -n "${IMDB_ID}" ]]; then
      EchoW "Both --actor and --imdb specified. IMDb ID will take precedence."
      ACTOR_QUERY=""
    fi
  fi
}

#==============================================================================
# DoDependency: Validate or install required tools with fallback and verbosity
#------------------------------------------------------------------------------
# Description:
#   Checks for required and optional dependencies:
#     - Required: curl, jq, date, urlencode
#     - Optional (feature-specific): pup
#
#   Mode of operation is controlled by:
#     - CHECK_DEPENDENCIES: verify and report missing (no fallback/install)
#     - CHECK_DEPS_LOCAL: check and fallback if possible (e.g., DefineUrlencodeFallback)
#     - CHECK_DEPS_INSTALL: force install mode (assumes package manager is available)
#     - VERBOSE_DEPS: if set, displays version info for available tools
#
# Globals Used:
#   CHECK_DEPENDENCIES
#   CHECK_DEPS_LOCAL
#   CHECK_DEPS_INSTALL
#   VERBOSE_DEPS
#
# Outputs:
#   - Echo output for each tool, including version or status
#   - Fallback logic invoked for 'urlencode' if missing and local mode enabled
#   - Exits with code 1 if any required tool is missing and not handled
#==============================================================================
DoDependency() {
  local -a required_tools=("curl" "jq" "date" "urlencode")
  local -a optional_tools=("pup")
  local -i missing=0

  if (( CHECK_DEPS_INSTALL )); then
    Echo "🔧 Forcing install of all required tools..."
    for tool in "${required_tools[@]}"; do
      EnsureDependency "${tool}" "${tool}"
    done
    for opt in "${optional_tools[@]}"; do
      EnsureDependency "${opt}" "${opt}" "optional"
    done
    Echo "✅ All required tools have been installed or handled."

  elif (( CHECK_DEPS_LOCAL )); then
    Echo "🔍 Checking tools and applying local fallback logic (no install)..."
    for tool in "${required_tools[@]}"; do
      if command -v "${tool}" &>/dev/null; then
        Echo "✔ '${tool}' available"
        (( VERBOSE_DEPS )) && "${tool}" --version 2>&1 | head -n 1 | sed 's/^/   ↪ /'
      else
        case "${tool}" in
          urlencode)
            DefineUrlencodeFallback
            Echo "ℹ️ Defined fallback for '${tool}'"
            ;;
          *)
            EchoE "❌ Required tool '${tool}' is missing and no fallback is available."
            (( missing++ ))
            ;;
        esac
      fi
    done

    for opt in "${optional_tools[@]}"; do
      if command -v "${opt}" &>/dev/null; then
        Echo "✔ Optional tool '${opt}' available"
        (( VERBOSE_DEPS )) && "${opt}" --version 2>&1 | head -n 1 | sed 's/^/   ↪ /'
      else
        EchoW "Optional tool '${opt}' is not available. Some features may be disabled."
      fi
    done

    if (( missing > 0 )); then
      EchoE "⛔ ${missing} required tool(s) could not be handled. Aborting."
      exit 1
    else
      Echo "✅ All required tools are available or have been handled with fallback."
    fi

  elif (( CHECK_DEPENDENCIES )); then
    Echo "🔍 Verifying dependencies only (no install, no fallback)..."
    for tool in "${required_tools[@]}"; do
      if command -v "${tool}" &>/dev/null; then
        Echo "✔ '${tool}' available"
        (( VERBOSE_DEPS )) && "${tool}" --version 2>&1 | head -n 1 | sed 's/^/   ↪ /'
      else
        EchoE "❌ Missing required tool: ${tool}"
        (( missing++ ))
      fi
    done

    for opt in "${optional_tools[@]}"; do
      if command -v "${opt}" &>/dev/null; then
        Echo "✔ Optional tool '${opt}' available"
        (( VERBOSE_DEPS )) && "${opt}" --version 2>&1 | head -n 1 | sed 's/^/   ↪ /'
      else
        EchoW "⚠️ Optional tool '${opt}' is not available. Some features may be disabled."
      fi
    done

    if (( missing > 0 )); then
      EchoE "⛔ Missing ${missing} required tool(s). Use --check-deps-local or --check-deps-install."
      exit 1
    else
      Echo "✅ All required tools are present."
    fi
  fi
}

#==============================================================================
# ParseImdbMiniBio: Extracts the Mini Bio HTML section from downloaded IMDb bio
#------------------------------------------------------------------------------
# Description:
#   Parses a downloaded IMDb biography HTML file and extracts the "Mini Bio"
#   section. It targets the block identified by 'data-testid="sub-section-mini_bio"'
#   and cleans the HTML into plain text. The result is saved to the output file.
#
# Arguments:
#   ${1} - Path to the input HTML file downloaded from IMDb
#   ${2} - Path to the output text file where the parsed bio should be saved
#
# Globals:
#   FUNCNAME - Used for logging/debug output
#
# Outputs:
#   Writes plain-text biography content to the specified output file.
#
# Exit Codes:
#   None explicitly. Errors may surface from sed or file access issues.
#==============================================================================
ParseImdbMiniBio() {
  local -r input_file="${1}"
  local -r output_file="${2}"

  # Attempt to extract bio inside 'data-testid="sub-section-mini_bio"' block
  sed -n '/data-testid="sub-section-mini_bio"/,/<\/section>/p' "${input_file}" |
    sed -n '/<div class="ipc-html-content-inner-div"/,/<\/div><\/div><\/div>/p' |
    sed 's/<[^>]*>//g; s/&quot;/"/g; s/&#39;/'\''/g; s/&nbsp;/ /g' |
    sed '/^[[:space:]]*$/d' > "${output_file}"
}

#==============================================================================
# EnsureActorMiniBioCached: Ensures IMDb Mini Bio is downloaded and cached
#------------------------------------------------------------------------------
# Description:
#   Downloads and caches the Mini Bio section from an actor's IMDb bio page.
#   Creates <imdb_id>_bio.db under ${MY_MDB_CACHE}/ containing the extracted
#   paragraph. Also appends a permalink to links.db if not already present.
#
# Globals:
#   MY_MDB_CACHE       - Path to the local metadata cache directory
#   FUNCNAME           - Used for debug/error messages
#   FORCE_BIO          - If set, redownload even if cached
#   ACTOR_BIO_HTML     - (Set) Parsed bio HTML loaded into memory
#==============================================================================
EnsureActorMiniBioCached() {
  local -r imdb_id="${1}"
  local -r mini_bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db"
  local -r imdb_bio_url="https://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm"
  local -r links_file="${MY_MDB_CACHE}/links.db"

  ACTOR_BIO_HTML=
  if (( ! FORCE_BIO )) && [[ -s "${mini_bio_file}" ]]; then
    ACTOR_BIO_HTML="$(< "${mini_bio_file}")"
    return 0
  fi

  local tmp_bio_html="$(mktemp)"

  # Use spoofed User-Agent to bypass 403
  #curl --silent --fail \
  #  --header "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" \
  #  "${imdb_bio_url}" > "${tmp_bio_html}"
  wget --quiet \
    --header="User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" \
    --output-document="${tmp_bio_html}" \
    "${imdb_bio_url}"

  ParseImdbMiniBio "${tmp_bio_html}" "${mini_bio_file}"
  rm -f "${tmp_bio_html}"

  # Validate and set memory cache
  if [[ ! -s "${mini_bio_file}" ]]; then
    EchoW "${FUNCNAME[0]}:${LINENO}: IMDb Mini Bio could not be extracted: ${mini_bio_file}"
    ACTOR_BIO_HTML="<p><em>Biography not available.</em></p>"
    echo "${ACTOR_BIO_HTML}" > "${mini_bio_file}"
  else
    ACTOR_BIO_HTML="$(< "${mini_bio_file}")"
    EchoD "${FUNCNAME[0]}:${LINENO}:✅ IMDb Mini Bio saved to ${mini_bio_file}"
  fi

  if ! grep -q "^${imdb_id}[[:space:]]+.*#mini_bio" "${LINKS_DB_PATH}" 2>/dev/null; then
    echo -e "${imdb_id}|https://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm#mini_bio" >> "${LINKS_DB_PATH}"
    EchoD "${FUNCNAME[0]}:${LINENO}:🔗 Added IMDb Mini Bio link to ${LINKS_DB_PATH}"
  fi
}

#==============================================================================
# GenerateActorFilmographyFromTmdb - Build full actor filmography HTML from TMDb
#------------------------------------------------------------------------------
# Description:
#   Retrieves filmography for an actor using TMDb API, falling back to local
#   cache when available. The output includes photo, bio, movie table, and
#   external links. Movies are fetched via movie_credits API and sorted by
#   release date. Metadata is stored in ~/.MyMDBCache/*.db files.
#
#   If DRYRUN is enabled, metadata will not be saved. Roles are sanitized for
#   HTML, and a red ❌ is shown for films released after the actor’s death.
#
# Globals:
#   ACTOR_IMDB_ID         - IMDb ID of the actor (RO)
#   ACTOR_QUERY           - Actor name for fallback messaging (RO)
#   ACTOR_NAME            - Actor name (RW, exported here)
#   ACTOR_DOB             - Date of birth, YYYY-MM-DD (RW, exported here)
#   ACTOR_DOD             - Date of death, if any (RW, exported here)
#   ACTOR_BIRTH_YEAR      - Year of birth as integer (RW, exported here)
#   ACTOR_PHOTO_URL       - Full TMDb profile image URL (RW, exported here)
#   ACTOR_BLOG_URL        - Cached Blogger post URL (RW, exported here)
#   ACTOR_TMDB_ID         - TMDb actor ID (RW, set internally)
#   ACTOR_FILMOGRAPHY     - HTML output block (RW, set by this function)
#   TMDB_API_KEY          - TMDb API key (RO)
#   MY_MDB_CACHE          - Path to ~/.MyMDBCache (RO)
#   LINKS_DB_PATH         - Path to links.db (RO)
#   DRYRUN                - If non-zero, avoids writing to cache (RO)
#   VERBOSE               - If non-zero, enables verbose output (RO)
#   DEBUG                 - If non-zero, enables debug logging (RO)
#
# Outputs:
#   Sets ACTOR_FILMOGRAPHY with HTML content including:
#     - Actor photo and details
#     - Chronological movie table (Title, Year, Role, Age, IMDb link, TMDb ID)
#     - External links from links.db
#     - Biography from <imdb>.bio.db
#   Logs messages via Echo, EchoW, EchoD to stdout
#
# Exit Codes:
#   0 - Success
#   1 - Failure to fetch TMDb ID, movie credits, or generate output
#==============================================================================
GenerateActorFilmographyFromTmdb() {
  local -r imdb_id="${ACTOR_IMDB_ID}"
  local record tmdb_id name dob dod photo_url blog_url
  local movie_tmdb_id movie_imdb_id country title original_title release watched_date movie_blogger_url movie_photo_url
  local role year age age_cell imdb_link tmdb_link
  local output=""
  local -r bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db"

  DBMovie_InitDb
  
  DBMovie_LoadActorRecordParse "" "${imdb_id}"
  EnsureActorMiniBioCached "${imdb_id}"

  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: ACTOR_PHOTO_URL='${ACTOR_PHOTO_URL}'"
  if [[ -n "${ACTOR_PHOTO_URL}" ]]; then
    output+="<div style=\"display:flex; align-items:flex-start; margin-bottom:20px;\">"
    output+="<img src=\"${ACTOR_PHOTO_URL}\" alt=\"${ACTOR_NAME}\" style=\"width:160px; margin-right:20px; border:1px solid #ccc;\" />"
    output+="<div style=\"font-size:16px; line-height:1.6;\">"
    output+="<strong>${ACTOR_NAME}</strong><br />"
    output+="Born: ${ACTOR_DOB}<br />"
    output+="Died: ${ACTOR_DOD}"
    output+="</div></div>"
  fi

  output+="<h2>Filmography</h2>"
  output+="<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">"
  output+="<tr><th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th><th>IMDb</th><th>TMDb</th></tr>"

  (( VERBOSE==0 )) && Echo "Processing movies. Use --verbose to see each movie name."
  (( VERBOSE>0  )) && Echo "Processing movies."
  #DBMovie_LoadMovieCreditByActorId "${imdb_id}" > /dev/null
  local -r actor_movies="$(DBMovie_LoadMoviesByActor "${ACTOR_TMDB_ID}" | sort -t'|' -k6)"
  while IFS='|' read -r movie_tmdb_id movie_imdb_id country title original_title release watched_date movie_blogger_url movie_photo_url; do
    (( DEBUG )) && EchoD "movie_tmdb_id='${movie_tmdb_id}'"
    role=$(DBMovie_GetMovieRole "${movie_tmdb_id}" "${ACTOR_TMDB_ID}")
    year="${release:0:4}"
    (( VERBOSE )) && Echo "  🎬 Processing: ${title} (${year})"

    # Start of a row
    output+="<tr>"
    
    # Movie release year
    output+="<td>${year}</td>"
    
    # Movie title, with link to my review in blogger
    if [[ -n "${movie_blogger_url}" ]]; then
      output+="<td><a href=\"https://afberendsen.blogspot.com/${movie_blogger_url}\" target=\"_blank\">${title}</a></td>"
    else
      output+="<td>${title}</td>"
    fi
    output+="<td>${role}</td>"

    # Actor age at movie
    age=""
    age_cell=""
    if [[ -n "${ACTOR_DOB}" && -n "${release}" ]]; then
      age=$(( ${release:0:4} - ${ACTOR_DOB:0:4} ))
      if [[ -n "${ACTOR_DOD}" && "${release}" > "${ACTOR_DOD}" ]]; then
        age_cell="❌"
      else
        age_cell="${age}"
      fi
    fi
    output+="<td>${age_cell}</td>"
    
    # Watched
    if [[ -n "${watched_date}" ]]; then
      output+="<td>✅</td>"
    else
      output+="<td></td>"
    fi
    
    # IMDb link, if any
    if [[ -n "${movie_imdb_id}" ]]; then
      output+="<td><a href=\"https://www.imdb.com/title/${movie_imdb_id}\" target=\"_blank\">${movie_imdb_id}</a></td>"
    else
      output+="<td><span style=\"color:#999;\">N/A</span></td>"
    fi
    
    # TMDb link, if any
    if [[ -n "${movie_tmdb_id}" ]]; then
      output+="<td><a href=\"https://www.themoviedb.org/movie/${movie_tmdb_id}\" target=\"_blank\">${movie_tmdb_id}</a></td>"
    else
      output+="<td><span style=\"color:#999;\">N/A</span></td>"
    fi
    
    # End of row
    output+="<tr>"
  done <<< "${actor_movies}"
  
  # End of filmography table
  output+="</tbody></table>"

  # Beginning of Links header
  output+="<h2>Links</h2><ul>"
  
  # 1. IMDb actor page
  output+="<li><a href=\"https://www.imdb.com/name/${imdb_id}\" target=\"_blank\">IMDb: ${ACTOR_NAME}</a></li>"

  # 2. TMDb actor page
  if [[ -n "${ACTOR_TMDB_ID}" ]]; then
    output+="<li><a href=\"https://www.themoviedb.org/person/${ACTOR_TMDB_ID}\" target=\"_blank\">TMDb: ${ACTOR_NAME}</a></li>"
  fi
  
  # 3. Additional custom links from links.db
  if [[ -f "${LINKS_DB_PATH}" ]]; then
    while read -r dummy url; do
      output+="<li><a href=\"${url}\" target=\"_blank\">${url}</a></li>"
    done < <(grep -E "^${imdb_id}[[:space:]]+" "${LINKS_DB_PATH}" | sort -u)
  else
    EchoW "No links.db file found at ${LINKS_DB_PATH}"
  fi
  output+="</ul>"

  output+="<h2>Bio</h2>"
  if [[ -f "${bio_file}" ]]; then
    output+="$(< "${bio_file}")"
  else
    EchoW "No biography found at ${bio_file}"
    output+="<p><em>Biography not available.</em></p>"
  fi

  ACTOR_FILMOGRAPHY="${output}"
}

#==============================================================================
# DoActor_0_Filmography: Generate and submit actor filmography to Blogger
#------------------------------------------------------------------------------
# Description:
#   This function handles the complete flow of building, previewing, or submitting
#   a Blogger post for an actor's filmography. It can use TMDb ID, IMDb ID, or
#   actor name as input. Prioritizes local cache and minimal online calls.
#
# Globals Used:
#   ACTOR_TMDB_ID, ACTOR_IMDB_ID, ACTOR_NAME
#   UPDATE_MODE, DEBUG, DRYRUN
#   MY_MDB_CACHE
#
# Globals Set:
#   ACTOR_FILMOGRAPHY
#
# Arguments:
#   $1 - TMDb actor ID (optional)
#   $2 - IMDb actor ID (optional)
#   $3 - Actor name (optional)
#
# Return:
#   0 - Success
#   1 - Failure (resolution error, cache error, generation error)
#==============================================================================
DoActor_0_Filmography() {
  local -r param_tmdb_id="$(SanitizeField "${1:-}")"
  local -r param_imdb_id="$(SanitizeField "${2:-}")"
  local -r param_actor_name="$(SanitizeField "${3:-}")"
  local -r FUNC="${FUNCNAME[0]}"

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: $*"

  # Validate at least one input
  if [[ -z "${param_tmdb_id}" && -z "${param_imdb_id}" && -z "${param_actor_name}" ]]; then
    EchoE "${FUNC}:${LINENO}: At least one parameter (TMDB ID, IMDb ID, or Name) must be provided."
    return 1
  fi

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
  # Resolve actor IDs
  if [[ -n "${param_tmdb_id}" || -n "${param_imdb_id}" ]]; then
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
    ResolveActorIds "${param_tmdb_id}" "${param_imdb_id}" || {
      EchoE "${FUNC}:${LINENO}: Failed to resolve actor IDs."
      return 1
    }
  elif [[ -n "${param_actor_name}" ]]; then
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
    GetActorTmdbIdFromName "${param_actor_name}" || {
      EchoE "${FUNC}:${LINENO}: Failed to resolve TMDb ID from actor name."
      return 1
    }
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID=${ACTOR_TMDB_ID}"
    if [[ "${ACTOR_TMDB_ID_RESULT}" == "${MANY_RECORD_MARK}" ]]; then
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID_RESULT == ***MANY***"
      if [[ -n "${param_tmdb_id}" ]]; then
        ACTOR_TMDB_ID="${param_tmdb_id}"
      elif [[ -n "${param_imdb_id}" ]]; then
        (( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID=${ACTOR_TMDB_ID}"
        ResolveActorIds "" "${param_imdb_id}" || return 1
      else
        EchoE "${FUNC}:${LINENO}: Multiple candidates found, but no clear ID to select."
        return 1
      fi
    else
      (( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID=${ACTOR_TMDB_ID}"
      ACTOR_TMDB_ID="${ACTOR_TMDB_ID_RESULT}"
    fi
    (( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID=${ACTOR_TMDB_ID}"
    ResolveActorIds "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" || return 1
  fi

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
  # Validate IDs are now known
  if [[ -z "${ACTOR_TMDB_ID}" || -z "${ACTOR_IMDB_ID}" ]]; then
    EchoE "${FUNC}:${LINENO}: Missing TMDb or IMDb ID after resolution."
    return 1
  fi

  (( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
  # Confirm actor record exists
  DBMovie_LoadActorRecord "${ACTOR_TMDB_ID}" >/dev/null || DBMovie_LoadActorRecord "${ACTOR_IMDB_ID}" >/dev/null || {
    EchoE "${FUNC}:${LINENO}: Actor not found in local cache."
    return 1
  }

  DBMovie_LoadActorRecordParse "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}"
  Echo "Generating filmography for: ${ACTOR_NAME:-Unknown Actor}"
  (( DEBUG )) && EchoD "${FUNC}:${LINENO}: IMDb ID: ${ACTOR_IMDB_ID}  TMDb ID: ${ACTOR_TMDB_ID}"

  local -r blogger_url="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")"
  local new_url=""

  HandleNewBloggerPost() {
    if [[ -n "${new_url}" ]]; then
      Echo "🌐 Blogger URL: ${new_url}"
      if (( DRYRUN == 0 )); then
        DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_url#*blogspot.com/}"
      else
        Echo "DRYRUN active: skipping Blogger URL cache update."
      fi
    else
      EchoE "${FUNC}:${LINENO}: Failed to create Blogger post."
      return 1
    fi
  }

  if [[ -n "${blogger_url}" && ${UPDATE_MODE} -eq 0 ]]; then
    while true; do
      Echo "Existing Blogger URL found: ${blogger_url}"
      Echo "Choose one of the following options:"
      Echo "  a) Copy HTML to clipboard"
      Echo "  b) Save HTML locally as ${ACTOR_IMDB_ID}.html"
      Echo "  c) Create a new Blogger post and update cache"
      Echo "  d) Cancel"
      EchoN "Your choice (a/b/c/d): "
      read -r choice
      choice="${choice:0:1}"
      choice="${choice,,}"
      choice="${choice// /}"

      case "${choice}" in
        a)
          GenerateActorFilmographyFromTmdb || return 1
          echo "${ACTOR_FILMOGRAPHY}" > /dev/clipboard
          Echo "Copied HTML to clipboard."
          return 0
          ;;
        b)
          GenerateActorFilmographyFromTmdb || return 1
          echo "${ACTOR_FILMOGRAPHY}" > "${ACTOR_IMDB_ID}.html"
          Echo "Saved HTML to ${ACTOR_IMDB_ID}.html"
          return 0
          ;;
        c)
          GenerateActorFilmographyFromTmdb || return 1
          new_url="$(SubmitBlogDraft "${ACTOR_FILMOGRAPHY}")"
          HandleNewBloggerPost || return 1
          break
          ;;
        d)
          Echo "Aborted by user."
          return 1
          ;;
        *)
          EchoE "Invalid choice '${choice}'. Try again."
          ;;
      esac
    done
  else
    # No URL or forced update
    GenerateActorFilmographyFromTmdb || return 1
    new_url="$(SubmitBlogDraft "${ACTOR_FILMOGRAPHY}")"
    HandleNewBloggerPost || return 1
  fi

  Echo "✅ Done: Post created or updated for IMDb ID '${ACTOR_IMDB_ID}'."
  return 0
}

#==============================================================================
# DoActor_1_ShowUrl: Display the current Blogger URL for a given actor
#------------------------------------------------------------------------------
# Description:
#   This function queries the actor.db cache to retrieve the Blogger URL
#   associated with the IMDb ID stored in ACTOR_IMDB_ID. It outputs the URL
#   directly, or a blank if no entry exists.
#
# Globals Used:
#   ACTOR_IMDB_ID         - IMDb ID of the actor (used as lookup key)
#   ACTOR_DB              - Full path to ~/.MyBlogCache/actor.db
#
# Outputs:
#   Echoes the Blogger URL to stdout if it exists, otherwise a blank URL notice.
#
# Exit Codes:
#   0 - Always succeeds; informational only
#==============================================================================
DoActor_1_ShowUrl() {
  local blogger_url
  blogger_url="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")"
  if [[ -n "${blogger_url}" ]]; then
    Echo "Current Blogger URL is '${blogger_url}'"
  else
    Echo "Current Blogger URL is ''"
  fi
}

#==============================================================================
# DoActor_2_DeleteUrl: Remove the Blogger URL associated with an actor
#------------------------------------------------------------------------------
# Description:
#   This function clears the Blogger URL field for the actor identified by
#   ACTOR_IMDB_ID in the actor.db cache file. It effectively disassociates
#   the actor from any previously posted Blogger URL.
#
# Globals Used:
#   ACTOR_IMDB_ID         - IMDb ID of the actor (used as lookup key)
#   ACTOR_DB              - Full path to ~/.MyBlogCache/actor.db (used internally)
#   actor_name            - Display name for echo output (optional, used for clarity)
#
# Outputs:
#   Echoes a confirmation message indicating the Blogger URL was cleared.
#
# Exit Codes:
#   0 - Always succeeds; informational only
#==============================================================================
DoActor_2_DeleteUrl() {
  Echo "Clearing Blogger URL for '${actor_name}'..."
  DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" ""
}

#==============================================================================
# DoActor_3_UpdateUrl: Update Blogger URL for actor and validate uniqueness
#------------------------------------------------------------------------------
# Description:
#   Updates the Blogger URL for the actor identified by ACTOR_IMDB_ID.
#   Ensures the new URL is valid, unique across all actors, and optionally
#   allows overwriting identical entries if FORCE is set. Logs all updates to
#   ~/.MyMDBCache/actor_url_changes.log for auditing.
#
# Globals Used:
#   ACTOR_IMDB_ID         - IMDb ID of the actor (used as the key)
#   ACTOR_DB              - Full path to actor.db (used to check uniqueness)
#   actor_name            - Actor's name (used for logging/display)
#   FORCE                 - If non-zero, allows update even if path is unchanged
#
# Arguments:
#   ${1} - New full Blogger URL (must start with https:// and contain blogspot.com)
#
# Outputs:
#   - Confirms update via Echo
#   - Logs change to ~/.MyMDBCache/actor_url_changes.log
#   - Warns or errors on malformed or duplicate URLs
#
# Exit Codes:
#   0 - Success or no change needed
#   1 - Invalid URL or duplication detected
#==============================================================================
DoActor_3_UpdateUrl() {
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: 1='${1}'"
  local new_url="${1}"

  if [[ -z "${new_url}" ]]; then
    EchoE "Missing Blogger URL for update."
    exit 1
  fi
  if [[ "${new_url}" != https://*blogspot.com/* ]]; then
    EchoE "Only HTTPS blogspot URLs are allowed."
    exit 1
  fi

  local new_path="${new_url#*blogspot.com/}"
  new_path="${new_path%%/}"  # Trim trailing slashes

  if [[ "${new_path}" == "${new_url}" ]]; then
    EchoE "Invalid Blogger URL format: ${new_url}"
    exit 1
  fi
  if [[ "${new_path}" =~ [[:space:]] ]]; then
    EchoW "Blogger path contains spaces; this may indicate a malformed URL."
  fi

  local current_path
  current_path="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")"
  current_path="${current_path%%/}"

  if [[ "${new_path,,}" == "${current_path,,}" ]]; then
    if (( FORCE )); then
      EchoD "FORCE active: proceeding with update of identical path."
    else
      EchoW "New URL is same as current; skipping update..."
      return 0
    fi
  fi

  # Check if another actor is using this path
  local path_owner
  path_owner="$(grep -i "|${new_path}$" "${ACTOR_DB}" | awk -F'|' '$1 != "'${ACTOR_IMDB_ID}'" {print $3}')"
  if [[ -n "${path_owner}" ]]; then
    EchoE "Another actor already uses this Blogger path: ${path_owner}"
    exit 1
  fi

  Echo "🌐 Updating Blogger URL for '${actor_name}'..."
  Echo "   Previous: '${current_path}'"
  Echo "   New     : '${new_path}'"

  # Save history
  local log="${HOME}/.MyMDBCache/actor_url_changes.log"
  local timestamp
  timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
  echo "${timestamp}|${ACTOR_IMDB_ID}|${actor_name}|${current_path}|${new_path}" >> "${log}"

  DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_path}"
  Echo "🌐 Current Blogger URL is '${new_path}'"
}

#==============================================================================
# DoActor_4_RevertUrl: Revert Blogger URL for actor from history
#------------------------------------------------------------------------------
# Description:
#   Reverts the Blogger URL associated with an actor (identified by ACTOR_IMDB_ID)
#   to a previous value using the actor URL change log at
#   ~/.MyMDBCache/actor_url_changes.log. The number of steps to revert can be
#   specified; default is 1 (revert to immediately previous URL).
#
# Globals Used:
#   ACTOR_IMDB_ID         - IMDb ID of the actor (used to identify history lines)
#   HOME                  - Used to locate the change history log under ~/.MyMDBCache
#
# Arguments:
#   ${1} - (Optional) Number of steps to revert back (default is 1)
#
# Outputs:
#   - Echo message indicating new (reverted) Blogger URL
#   - Error messages if log file is missing, entry not found, or step is invalid
#
# Exit Codes:
#   0 - Successful revert
#   1 - Error: log file missing, insufficient history, or malformed log entry
#==============================================================================
DoActor_4_RevertUrl() {
  local -i steps="${1:-1}"
  local log="${HOME}/.MyMDBCache/actor_url_changes.log"
  local -i count=0

  if [[ ! -f "${log}" ]]; then
    EchoE "No history log found."
    exit 1
  fi

  local lines
  lines="$(grep "|${ACTOR_IMDB_ID}|" "${log}" | tac)"
  if [[ -z "${lines}" ]]; then
    EchoE "No history for this actor."
    exit 1
  fi

  while IFS= read -r line; do
    (( count++ ))
    if (( count == steps )); then
      local prev_path
      prev_path="$(echo "${line}" | cut -d'|' -f4)"
      if [[ -z "${prev_path}" ]]; then
        EchoE "Previous URL path not found in history."
        exit 1
      fi
      Echo "Rolling back to '${prev_path}'..."
      DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${prev_path}"
      return 0
    fi
  done < <(echo "${lines}")

  EchoE "Unable to revert: fewer than ${steps} history entries found."
  exit 1
}

#==============================================================================
# DoActor_5_ShowUrlHistory: Display change history for actor Blogger URLs
#------------------------------------------------------------------------------
# Description:
#   Displays the timestamped history of Blogger URL changes for a specific actor
#   using the IMDb ID defined in ACTOR_IMDB_ID. It parses the
#   ~/.MyMDBCache/actor_url_changes.log file and presents each historical update
#   as a timestamped transition from old to new URL path.
#
# Globals Used:
#   ACTOR_IMDB_ID         - IMDb ID of the actor (used to match log entries)
#   HOME                  - Used to resolve log path at ~/.MyMDBCache/
#
# Outputs:
#   - Prints timestamp, previous URL path, and updated URL path for each entry
#   - EchoE warning if the log file is missing
#
# Exit Codes:
#   0 - Success (entries printed or empty result)
#   1 - Error: history log not found
#==============================================================================
DoActor_5_ShowUrlHistory() {
  local log="${HOME}/.MyMDBCache/actor_url_changes.log"
  if [[ ! -f "${log}" ]]; then
    EchoE "No history file found."
    exit 1
  fi

  grep "|${ACTOR_IMDB_ID}|" "${log}" | while IFS='|' read -r ts imdb name old new; do
    printf "🕓 %s: %s → %s\n" "${ts}" "${old}" "${new}"
  done
}

#==============================================================================
# DoActor_6_ShowPosterUrl: Show the current photo URL associated with an actor
#------------------------------------------------------------------------------
# Globals:
#   ACTOR_IMDB_ID         - IMDb ID of the actor
#   ACTOR_DB              - Path to actor.db
# Outputs:
#   Prints current photo URL for the actor or a notice if none exists
#==============================================================================
DoActor_6_ShowPosterUrl() {
  local photo_url
  photo_url="$(DBMovie_GetActorPhotoUrl "${ACTOR_IMDB_ID}")"
  if [[ -n "${photo_url}" ]]; then
    Echo "Current photo URL is '${photo_url}'"
  else
    Echo "Current photo URL is ''"
  fi
}

#==============================================================================
# DoActor_7_DeletePosterUrl: Remove the photo URL associated with an actor
#------------------------------------------------------------------------------
# Globals:
#   ACTOR_IMDB_ID         - IMDb ID of the actor
# Outputs:
#   Deletes photo URL field for actor in cache and confirms the action
#==============================================================================
DoActor_7_DeletePosterUrl() {
  Echo "Clearing photo URL for '${actor_name}'..."
  DBMovie_UpdateActorPhotoUrl "${ACTOR_IMDB_ID}" ""
}

#==============================================================================
# DoActor_8_UpdatePosterUrl: Update photo URL for actor and validate uniqueness
#------------------------------------------------------------------------------
# Globals:
#   ACTOR_IMDB_ID         - IMDb ID of the actor
#   FORCE                 - If set, allow identical URL update
#   ACTOR_DB              - Path to actor.db
# Arguments:
#   ${1} - New full photo URL (must start with https://)
# Outputs:
#   Updates photo URL for the actor, logs change history, and prints new state
#==============================================================================
DoActor_8_UpdatePosterUrl() {
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: 1='${1}'"
  local new_url="${1}"

  if [[ -z "${new_url}" ]]; then
    EchoE "Missing photo URL for update."
    exit 1
  fi
  if [[ "${new_url}" != https://* ]]; then
    EchoE "Only HTTPS URLs are allowed."
    exit 1
  fi

  local current_url
  current_url="$(DBMovie_GetActorPhotoUrl "${ACTOR_IMDB_ID}")"

  if [[ "${new_url}" == "${current_url}" ]]; then
    if (( FORCE )); then
      EchoD "FORCE active: proceeding with update of identical photo URL."
    else
      EchoW "New URL is same as current; skipping update..."
      return 0
    fi
  fi

  Echo "🖼️  Updating photo URL for '${actor_name}'..."
  Echo "   Previous: '${current_url}'"
  Echo "   New     : '${new_url}'"

  local log="${HOME}/.MyMDBCache/actor_photo_changes.log"
  local timestamp
  timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
  echo "${timestamp}|${ACTOR_IMDB_ID}|${actor_name}|${current_url}|${new_url}" >> "${log}"

  DBMovie_UpdateActorPhotoUrl "${ACTOR_IMDB_ID}" "${new_url}"
  Echo "🖼️  Current photo URL is '${new_url}'"
}

#==============================================================================
# DoActor_9_RevertPosterUrl: Revert actor photo URL from history
#------------------------------------------------------------------------------
# Globals:
#   ACTOR_IMDB_ID         - IMDb ID of the actor
#   HOME                  - Used to locate history log at ~/.MyMDBCache/
# Arguments:
#   ${1} - Number of steps to go back (default: 1)
# Outputs:
#   Reverts actor photo URL if history available and prints result
#==============================================================================
DoActor_9_RevertPosterUrl() {
  local -i steps="${1:-1}"
  local log="${HOME}/.MyMDBCache/actor_photo_changes.log"
  local -i count=0

  if [[ ! -f "${log}" ]]; then
    EchoE "No photo URL history log found."
    exit 1
  fi

  local lines
  lines="$(grep "|${ACTOR_IMDB_ID}|" "${log}" | tac)"
  if [[ -z "${lines}" ]]; then
    EchoE "No photo URL history for this actor."
    exit 1
  fi

  while IFS= read -r line; do
    (( count++ ))
    if (( count == steps )); then
      local prev_url
      prev_url="$(echo "${line}" | cut -d'|' -f4)"
      if [[ -z "${prev_url}" ]]; then
        EchoE "Previous photo URL not found in history."
        exit 1
      fi
      Echo "Rolling back to '${prev_url}'..."
      DBMovie_UpdateActorPhotoUrl "${ACTOR_IMDB_ID}" "${prev_url}"
      return 0
    fi
  done < <(echo "${lines}")

  EchoE "Unable to revert: fewer than ${steps} history entries found."
  exit 1
}

#==============================================================================
# DoActor_A_ShowPosterUrlHistory: Display change history for actor photo URLs
#------------------------------------------------------------------------------
# Globals:
#   ACTOR_IMDB_ID         - IMDb ID of the actor
#   HOME                  - Used to locate history log at ~/.MyMDBCache/
# Outputs:
#   Displays all historical photo URL changes for the specified actor
#==============================================================================
DoActor_A_ShowPosterUrlHistory() {
  local log="${HOME}/.MyMDBCache/actor_photo_changes.log"
  if [[ ! -f "${log}" ]]; then
    EchoE "No photo URL history file found."
    exit 1
  fi

  grep "|${ACTOR_IMDB_ID}|" "${log}" | while IFS='|' read -r ts imdb name old new; do
    printf "🕓 %s: %s → %s\n" "${ts}" "${old}" "${new}"
  done
}

#==============================================================================
# DoActor: Executes actor-related operations based on submode
#------------------------------------------------------------------------------
# Description:
#   Routes execution to one of the DoActor_* functions depending on the selected
#   EXECUTION_SUBMODE. This is the main dispatcher for actor-related operations,
#   including generating filmography HTML, showing or updating Blogger URLs,
#   reverting URL changes, and displaying change history.
#
# Globals Used:
#   EXECUTION_SUBMODE     - Mode selector (0=create, 1=show URL, 2=delete, 3=update, 4=revert, 5=history)
#   ACTOR_QUERY           - Name of the actor (may be overridden by IMDB_ID)
#   IMDB_ID               - Optional IMDb ID of the actor (takes precedence over ACTOR_QUERY)
#   ACTOR_IMDB_ID         - Set internally after resolving actor identity
#   NEW_BLOGGER_URL       - Used in update mode to set new Blogger path
#   REVERT_STEPS          - Used in revert mode to control rollback depth
#   DEBUG                 - Enables diagnostic logging
#
# Arguments:
#   ${1} - Submode to execute (0 = filmography, 1 = show, 2 = delete, 3 = update, 4 = revert, 5 = history)
#   ${2} - Optional argument depending on the submode (e.g., new URL or number of steps)
#
# Exit Codes:
#   0 - Success
#   1 - Failure (e.g., unknown mode, failed resolution, missing actor record)
#==============================================================================
DoActor() {
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: $*"
  local -r mode="${1:-0}"

  # Ensure DBs are initialized
  DBMovie_InitDb || return 1

#  # Determine which input to use
#  local actor_ref=""
#  if [[ -n "${IMDB_ID}" ]]; then
#    actor_ref="${IMDB_ID}"
#  elif [[ -n "${ACTOR_QUERY}" ]]; then
#    actor_ref="${ACTOR_QUERY}"
#  else
#    EchoE "${FUNCNAME[0]}:${LINENO}: No actor reference (IMDB_ID or ACTOR_QUERY) provided."
#    return 1
#  fi
#
#  # Try resolving TMDb/IMDb IDs
#  ResolveActorIdFromNameOnline "${actor_ref}" || {
#    EchoE "${FUNCNAME[0]}:${LINENO}: Failed to resolve actor identity for '${actor_ref}'."
#    return 1
#  }
#
#  # Now ACTOR_TMDB_ID and ACTOR_IMDB_ID must be filled
#  if [[ -z "${ACTOR_TMDB_ID}" || -z "${ACTOR_IMDB_ID}" ]]; then
#    EchoE "${FUNCNAME[0]}:${LINENO}: Missing TMDb or IMDb ID after resolution."
#    return 1
#  fi
#
#  # Ensure actor record is present locally
#  DBMovie_LoadActorRecord "${ACTOR_TMDB_ID}" || DBMovie_LoadActorRecord "${ACTOR_IMDB_ID}" || {
#    EchoE "${FUNCNAME[0]}:${LINENO}: Actor not found in local cache (TMDb ${ACTOR_TMDB_ID} or IMDb ${ACTOR_IMDB_ID})."
#    return 1
#  }

  # Dispatch to the appropriate mode handler
  case "${mode}" in
    0)  DoActor_0_Filmography          "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" "${ACTOR_QUERY}" ;;
    1)  DoActor_1_ShowUrl              "${2}" ;;
    2)  DoActor_2_DeleteUrl            "${2}" ;;
    3)  DoActor_3_UpdateUrl            "${2}" ;;
    4)  DoActor_4_RevertUrl            "${2}" ;;
    5)  DoActor_5_ShowUrlHistory       "${2}" ;;
    6)  DoActor_6_ShowPosterUrl        "${2}" ;;
    7)  DoActor_7_DeletePosterUrl      "${2}" ;;
    8)  DoActor_8_UpdatePosterUrl      "${2}" ;;
    9)  DoActor_9_RevertPosterUrl      "${2}" ;;
    10) DoActor_A_ShowPosterUrlHistory "${2}" ;;
    *)
      EchoE "${FUNCNAME[0]}:${LINENO}: Invalid execution mode '${mode}'."
      return 1
      ;;
  esac

  return 0
}

#==============================================================================
# DoMovieList: Scans movie directories and posts updates to Blogger
#------------------------------------------------------------------------------
# Description:
#   Determines the effective REFERENCE_DATE (either today's date or CUSTOM_DATE
#   passed via --date). Scans for updates to movie directories, filters results
#   by year if applicable, builds HTML tables and summaries, and submits a
#   Blogger draft post containing the update.
#
# Globals Used:
#   CUSTOM_DATE            - Optional override for the reference date
#   FORCE_PUBLISH          - If set, overrides date/year warnings
#   FORCE                  - Bypasses user confirmation prompt
#   FILTER_YEARS[]         - Optional list of years to filter updates
#   REFERENCE_DATE         - Set here based on CUSTOM_DATE or today
#   REFERENCE_DATE_EPOCH   - Epoch timestamp for REFERENCE_DATE
#   TODAY_EPOCH            - Epoch timestamp for REFERENCE_DATE + 1 day
#   year_map[]             - Filled by ScanDirectories with year → updated movies
#
# Outputs:
#   Blogger draft post submission with updated titles and summary
#
# Exit Codes:
#   0 - Success
#   1 - Invalid date format or aborted by user
#==============================================================================
DoMovieList() {
  # Validate and calculate effective date
  if [[ -z "${CUSTOM_DATE}" ]]; then
    EFFECTIVE_DATE=$(date --date=REFERENCE_DATE +%Y-%m-%d)
  else
    if ! date --date="${CUSTOM_DATE}" &>/dev/null; then
      Echo "ERROR: Invalid date format for --date: ${CUSTOM_DATE}"; exit 1
    fi
    CUSTOM_EPOCH=$(date --date="${CUSTOM_DATE} 00:00:00" +%s)
    TODAY_EPOCH=$(date --date="today 00:00:00" +%s)
    if (( CUSTOM_EPOCH >= TODAY_EPOCH )); then
      Echo "ERROR: Specified date must be before today."; exit 1
    fi
    EFFECTIVE_DATE=$(date --date="${CUSTOM_DATE}" +%Y-%m-%d)
  fi

  readonly REFERENCE_DATE="${EFFECTIVE_DATE}"
  readonly REFERENCE_DATE_EPOCH=$(date --date="${EFFECTIVE_DATE} 00:00:00" +%s)
  readonly TODAY_EPOCH=$(date --date="${EFFECTIVE_DATE} 00:00:00 + 1 day" +%s)

  if (( FORCE_PUBLISH == 1 && FORCE == 0 )); then
    if (( FORCE_PUBLISH == 1 && FORCE == 0 && ${#FILTER_YEARS[@]} > 0 )); then
      EchoW "⚠️  WARNING: You specified --publish AND limited the year(s) with --year/--range."
      EchoW "   The post will not reflect updates from all years. Publishing it could be misleading."
      EchoW "   Use --force to suppress this warning."
      read -p "Do you still want to proceed? (y/n): " answer
      [[ ! "${answer}" =~ ^[Yy]$ ]] && { Echo "Aborting by user request."; exit 1; }
    fi
  fi

  GetBlogAccessToken || {
    EchoE "Failed to retrieve Blogger API access token."
    return 1
  }
  ValidateTmdbKey
  ScanDirectories

  if [[ ${#year_map[@]} -eq 0 ]]; then
    Echo "No updated titles found for ${REFERENCE_DATE}."
  else
    html_output="$(BuildHtmlTable)$(BuildSummaryHtml)"
    SubmitBlogDraft "${html_output}"
    Echo "Done."
  fi
}

#==============================================================================
# Main: Entry point of script. Parses arguments and dispatches mode logic
#------------------------------------------------------------------------------
# Description:
#   Main dispatcher for the script. Parses all command-line arguments, validates
#   dependencies, and routes to the appropriate execution mode (movie or actor).
#   Supports multiple submodes for actor operations such as filmography posting,
#   URL management, and history reversion.
#
# Globals:
#   EXECUTION_MODE        - Primary mode of operation (1=movie-list, 2=actor)
#   EXECUTION_SUBMODE     - Actor submode (0–5)
#   NEW_BLOGGER_URL       - Passed to actor submode 3 (update)
#   REVERT_STEPS          - Passed to actor submode 4 (revert)
#   DRYRUN                - If set, suppresses blog submission
#
# Arguments:
#   All command-line parameters passed to the script
#
# Exit Codes:
#   0 - Success
#   1 - Invalid mode or submode, or argument parsing failure
#==============================================================================
Main() {
  if DoDependency && ParseArguments "$@" && DBMovie_InitDb; then
    case ${EXECUTION_MODE} in
      1) RunMovieUpdate ;;  # Movie list mode
      2) case ${EXECUTION_SUBMODE} in
         0|1|2|5|6|7|10) DoActor ${EXECUTION_SUBMODE} ;;
         3)              DoActor ${EXECUTION_SUBMODE} "${NEW_BLOGGER_URL}" ;;
         4)              DoActor ${EXECUTION_SUBMODE} "${REVERT_STEPS:-1}" ;;
         8)              DoActor ${EXECUTION_SUBMODE} "${NEW_PHOTO_URL}" ;;
         9)              DoActor ${EXECUTION_SUBMODE} "${REVERT_STEPS:-1}" ;;
         *)              EchoE "Invalid --actor execution mode ${EXECUTION_SUBMODE}" ; exit 1 ;;
         esac
         ;;
      *) EchoE "Invalid execution mode" ; exit 1 ;;
    esac
    return 0
  fi
  return 1
}

Main "$@" 
exit $?

# -----------------------------------------------------------------------------
# __CHATGPT_INSTRUCTIONS_BEGIN__
#
# All Bash functions in this script must include a header block with the following format:
#
#==============================================================================
# <function-name> - <short description of what the function does>
# Globals:
#   <VARIABLE_NAME> - Description of its use (RO for read-only, RW for read-write)
#   ...
# Arguments:
#   $1 - Description of the first positional argument
#   $2 - Description of the second positional argument (if applicable)
#   ...
# Outputs:
#   <Clearly state what is written to stdout or stderr>
#   <Any global variables modified (e.g., sets ACTOR_FILMOGRAPHY)>
#==============================================================================
#
# Guidelines:
# - Always use `local` for function-local variables.
# - Use `${...}` consistently for all variable references.
# - Output user-facing messages using the Echo, EchoE, EchoW, EchoD functions.
# - When writing a function that returns data via stdout, prefer using a dedicated file descriptor (e.g., `>&5`) to avoid collisions with logging.
# - Each function should be self-contained and document every global variable it accesses or modifies.
# - Always wrap long `jq` or `curl` commands in clean, readable syntax with fallbacks or clear error handling.
#
# Logging functions are predefined and must be used instead of raw `echo`.
# Each automatically includes a timestamp, an icon, and a log-level prefix.
#
# Use the following functions consistently:
#
# - `Echo`   → for normal informational output (✅ or 📌)
# - `EchoW`  → for warnings (⚠️ WARNING:)
# - `EchoE`  → for errors (❌ ERROR:)
# - `EchoD`  → for debug output (🛠 DEBUG:)
#
# Important:
# - Do **NOT** prepend messages manually with "ERROR:", "WARNING:", or emojis — these are automatically included by the functions.
# - Do **NOT** call `echo` directly for user-facing output unless it's data being piped, stored, or exported.
# - All log messages should be clear, human-readable, and aligned with script actions.
#
# Examples (✅ correct):
#   Echo    "✅ Submitted successfully."       # icon can be supplied
#   EchoW   "Actor already exists in the database."
#   EchoE   "Could not resolve TMDb ID for IMDb ID '${id}'."
#   EchoD   "Parsed movie title: ${title}"
#
# Examples (❌ incorrect):
#   EchoW   "WARNING: Actor exists."                # redundant prefix
#   EchoE   "❌ ERROR: Failed to post."            # message is duplicated
#   EchoD   "2025-04-22 00:51:35| Parsed movie title: ${title}"  # message havs two time stamps
#
# 1. **Purpose and Modes**:
#    - The script supports four modes: --delete, --recheck-error, --set-priority, and --manage-downloads.
#    - --delete: Deletes torrents matching patterns in TV_Organize_Patterns.txt.
#    - --recheck-error: Rechecks torrents in an "error" state for a specific qBittorrent instance.
#    - --set-priority: Sets high priority and force-starts TV torrents matching specific prefixes.
#    - --manage-downloads: Moves completed torrents to a final directory and ensures incomplete ones are in a temp directory.
#
# 2. **Key Variables**:
#    - PREFIXES: Array of TV show prefixes used for matching in --set-priority mode.
#    - QBT_INSTANCES: Associative array mapping instance names (tv, anime, etc.) to qBittorrent ports.
#    - TEMP_DIRS/FINAL_DIRS: Directory mappings for --manage-downloads mode.
#    - DRY_RUN: Flag to simulate actions (default: 1, enabled).
#
# 3. **Dependencies**:
#    - Requires curl, jq, date, and urlencode (with fallbacks for urlencode).
#    - Dependency checks are configurable via --check-deps, --check-deps-local, --check-deps-install.
#
# 4. **Logging and Debugging**:
#    - Use --debug (-D) to enable verbose output, including curl commands and intermediate data.
#    - Logs are written to /cygdrive/f/p_qBittorrent/.logs for reachability and failures.
#
# 5. **Error Handling**:
#    - The script includes reachability checks for qBittorrent with retries.
#    - Temporary files are cleaned up on exit (via trap).
#    - API responses are checked for failures.
#
# 6. **Potential Improvements**:
#    - Add support for more qBittorrent API endpoints (e.g., pausing/resuming torrents).
#    - Enhance pattern matching with regex options in --delete mode.
#    - Add a --force option to skip user prompts in --delete mode.
#
# 7. **Known Issues**:
#    - jq 1.7.1 has a bug causing assertion failures with complex expressions. Workarounds include simplifying jq usage or upgrading to a newer version.
#    - Large torrent lists may cause performance issues; consider batching API calls.
#
# __CHATGPT_INSTRUCTIONS_END__
# -----------------------------------------------------------------------------


Comments

Popular posts from this blog

TV Series - The Brokenwood Mysteries [NZ] (2014) - Season 10

 

Movie - Sin City: A Dame to Kill For (2014)

 

Movies - Deadpool & Wolverine (2024)