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

IT - My Home Platform View - All Infrastructure

Some ideas Deploy a harverster cluster Deploy a rancher server

Movie - The Gray Man (2022)

  My views Plot In 2003, senior  CIA  official Donald Fitzroy visits a prisoner named Courtland Gentry in Florida. Eight years earlier, Courtland was a minor convicted of killing his abusive father to protect his brother. Fitzroy offers him his freedom in exchange for working as an assassin in the CIA's  Sierra  program, an elite black ops unit, which will allow him to exist in the gray. In 2021, Courtland, now known as  Sierra Six , is working with fellow CIA agent Dani Miranda to assassinate a target named Dining Car suspected of selling off  national security  secrets in  Bangkok  during the national  Songkran  festival. Unable to do so stealthily without harming civilians, he attacks Dining Car directly, mortally wounding him. Before dying, he reveals he was also in the Sierra program as Sierra Four. He hands Six an encrypted drive detailing the corruption of CIA official Denny Carmichael, the lead agent on the assassinatio...

TV Mini-serie - Lady in the Lake (2024)