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
#==============================================================================

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

#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 -gi FORCE_BIO=0

# 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 
# Ensure local cache folder exists
# ==============================================================================
# Cache location definitions
# ==============================================================================
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"

#==============================================================================
# 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:
#
#     IMDB_ID|TMDB_ID|NAME|DOB|DOD|PHOTO_URL|BLOGGER_URL
#
#   This file is used to cache actor metadata retrieved from TMDb and Blogger.
#
# 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 - Always
#==============================================================================
DBMovie_InitActorDb() {
  mkdir -p "$(dirname "${ACTOR_DB_PATH}")"
  if [[ ! -s "${ACTOR_DB_PATH}" ]]; then
    echo "IMDB_ID|TMDB_ID|NAME|DOB|DOD|PHOTO_URL|BLOGGER_URL" > "${ACTOR_DB_PATH}"
    (( DEBUG )) && EchoD "[DBMovie_InitActorDb] Created ${ACTOR_DB_PATH} with header"
  fi
}

#==============================================================================
# DBMovie_GetActorDbPath: Return the absolute path to the actor.db cache
#------------------------------------------------------------------------------
# Description:
#   Returns the full path to the actor database file used for storing cached
#   metadata about actors. This includes fields like IMDb ID, TMDb ID, name,
#   birth/death dates, photo URL, and Blogger post URL.
#
# Globals Used:
#   ACTOR_DB_PATH - Path to the actor.db file (typically under ${HOME}/.MyBlogCache)
#
# Outputs:
#   Echoes the actor.db path to stdout
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_GetActorDbPath() {
  echo "${ACTOR_DB_PATH}"
}

#==============================================================================
# DBMovie_LoadActorRecord: Load the full actor record from actor.db
#------------------------------------------------------------------------------
# Description:
#   Retrieves a single actor's metadata entry from the local actor.db file
#   using their IMDb ID as the key. The actor.db contains one tab-separated
#   line per actor, with fields in the following order:
#
#     IMDb_ID, TMDb_ID, Name, DOB, DOD, Photo_URL, Blogger_URL
#
#   If no match is found, the function returns a non-zero status code.
#
# Arguments:
#   ${1} - IMDb ID (e.g., nm0000493)
#
# Globals Used:
#   ACTOR_DB_PATH - Resolved via DBMovie_GetActorDbPath
#
# Outputs:
#   Prints a tab-separated actor record if found:
#     <IMDb_ID>\t<TMDb_ID>\t<Name>\t<DOB>\t<DOD>\t<Photo_URL>\t<Blogger_URL>
#
# Return:
#   0 - If the actor record is found
#   1 - If no matching record is found
#==============================================================================
DBMovie_LoadActorRecord() {
  local -r imdb_id="$1"
  local -r file="$(DBMovie_GetActorDbPath)"
  grep -P "^${imdb_id}\t" "${file}" || return 1
}

#==============================================================================
# DBMovie_SaveActorRecord: Adds or updates an actor entry in actor.db
#------------------------------------------------------------------------------
# Description:
#   Adds a new actor record or updates an existing one in the local actor.db
#   metadata cache. If the IMDb ID already exists, the entry is replaced.
#   Fields are tab-separated and written in the following order:
#
#     IMDb_ID, TMDb_ID, Name, DOB, DOD, Photo_URL, Blogger_URL
#
#   Values are sanitized to strip tabs and linebreaks before writing.
#
# Usage:
#   DBMovie_SaveActorRecord <IMDb_ID> <TMDb_ID> <Name> <DOB> <DOD> <PhotoURL> <BlogURL>
#
# Arguments:
#   ${1} - IMDb ID (e.g., nm0000493)
#   ${2} - TMDb ID (e.g., 12345)
#   ${3} - Actor name (e.g., Jack Lemmon)
#   ${4} - Date of birth (YYYY-MM-DD)
#   ${5} - Date of death (YYYY-MM-DD or empty)
#   ${6} - Photo URL (full TMDb image path)
#   ${7} - Blogger post URL (or empty string)
#
# Globals Used:
#   ACTOR_DB_PATH - Retrieved via DBMovie_GetActorDbPath
#
# Outputs:
#   - Writes updated actor.db file to disk
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_SaveActorRecord() {
  local imdb_id="$1"
  local tmdb_id="$2"
  local name="$3"
  local dob="$4"
  local dod="$5"
  local photo_url="$6"
  local blog_url="$7"
  local -r file="$(DBMovie_GetActorDbPath)"

  # Sanitize
  name="${name//[$'\t\r\n']}"
  photo_url="${photo_url//[$'\t\r\n']}"
  blog_url="${blog_url//[$'\t\r\n']}"

  # Remove existing entry and add new one
  grep -v -P "^${imdb_id}\t" "${file}" > "${file}.tmp" 2>/dev/null || true
  printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "${imdb_id}" "${tmdb_id}" "${name}" "${dob}" "${dod}" "${photo_url}" "${blog_url}" >> "${file}.tmp"
  mv "${file}.tmp" "${file}"
}

#==============================================================================
# DBMovie_UpdateActorBloggerUrl: Update the Blogger URL field in actor.db
#------------------------------------------------------------------------------
# Description:
#   Loads the full actor record from actor.db using the provided IMDb ID,
#   replaces the Blogger URL field with the given new value, and writes the
#   updated record back to the actor.db file.
#
#   All other metadata fields (TMDb ID, name, DOB, etc.) are preserved.
#
# Arguments:
#   ${1} - IMDb ID (e.g., nm0000493)
#   ${2} - New Blogger URL path suffix (e.g., 2025/03/actor-jack-lemmon-1925-2001)
#
# Globals Used:
#   ACTOR_DB_PATH - Resolved internally via DBMovie_LoadActorRecord and SaveActorRecord
#
# Outputs:
#   - Updates the actor.db file with the new Blogger URL
#
# Return:
#   0 - Success
#   1 - If the actor record is not found
#==============================================================================
DBMovie_UpdateActorBloggerUrl() {
  local -r imdb_id="$1"
  local -r new_url="$2"
  local record
  record=$(DBMovie_LoadActorRecord "${imdb_id}") || return 1
  IFS=$'\t' read -r -a fields <<< "${record}"
  DBMovie_SaveActorRecord "${fields[0]}" "${fields[1]}" "${fields[2]}" "${fields[3]}" "${fields[4]}" "${fields[5]}" "${new_url}"
}

#==============================================================================
# DBMovie_UpdateActorPhotoUrl: Update the actor's photo URL in actor.db
#------------------------------------------------------------------------------
# Description:
#   Retrieves an actor record from actor.db using the provided IMDb ID,
#   updates the photo URL field with a new value, and rewrites the modified
#   record back to the actor metadata file. All other fields remain unchanged.
#
# Arguments:
#   ${1} - IMDb ID (e.g., nm0000493)
#   ${2} - New photo URL (full TMDb path or other image link)
#
# Globals Used:
#   ACTOR_DB_PATH - Used indirectly via DBMovie_LoadActorRecord and SaveActorRecord
#
# Outputs:
#   - Modifies the actor.db file with the new photo URL
#
# Return:
#   0 - Success
#   1 - If no record is found for the given IMDb ID
#==============================================================================
DBMovie_UpdateActorPhotoUrl() {
  local -r imdb_id="$1"
  local -r new_url="$2"
  local record
  record=$(DBMovie_LoadActorRecord "${imdb_id}") || return 1
  IFS=$'\t' read -r -a fields <<< "${record}"
  DBMovie_SaveActorRecord "${fields[0]}" "${fields[1]}" "${fields[2]}" "${fields[3]}" "${fields[4]}" "${new_url}" "${fields[6]}"
}

#==============================================================================
# DBMovie_SaveActorMetadata: Complete overwrite of actor record in actor.db
#------------------------------------------------------------------------------
# Description:
#   Performs a full overwrite of an actor's metadata entry in actor.db using
#   the provided fields. Any existing record for the given IMDb ID is removed
#   and replaced. This function serves as a parallel to DBMovie_SaveActorRecord
#   but operates independently of record-loading helpers.
#
#   Fields are stored as a tab-separated line in this order:
#     IMDb_ID, TMDb_ID, Name, DOB, DOD, Photo_URL, Blogger_URL
#
# Arguments:
#   ${1} - IMDb ID (e.g., nm0000493)
#   ${2} - TMDb ID (e.g., 12345)
#   ${3} - Actor name (e.g., Sylvester Stallone)
#   ${4} - Date of birth (YYYY-MM-DD)
#   ${5} - Date of death (optional, may be empty)
#   ${6} - Photo URL (full image path from TMDb or other source)
#   ${7} - Blogger post URL (may be empty)
#
# Globals Used:
#   MY_MDB_CACHE   - Root path for metadata caching
#   ACTOR_DB_PATH  - Full path to actor.db
#
# Outputs:
#   - Writes or replaces a single line in actor.db with new metadata
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_SaveActorMetadata() {
  local imdb_id="${1}"
  local tmdb_id="${2}"
  local name="${3}"
  local dob="${4}"
  local dod="${5}"
  local photo_url="${6}"
  local blog_url="${7}"

  mkdir -p "${MY_MDB_CACHE}"
  local cache_file="${ACTOR_DB_PATH}"

  # Remove any previous line and insert new
  if [[ -f "${cache_file}" ]]; then
    grep -v -P "^${imdb_id}\t" "${cache_file}" > "${cache_file}.tmp" || true
    mv "${cache_file}.tmp" "${cache_file}"
  fi

  echo -e "${imdb_id}\t${tmdb_id}\t${name}\t${dob}\t${dod}\t${photo_url}\t${blog_url}" >> "${cache_file}"
}

#==============================================================================
# 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 tmdb_id="${1}"
  local output=""
  local tmpfile
  tmpfile=$(mktemp)

  local 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>"

  jq -r '.cast[] | select(.release_date != null and .title != null) |
         [.release_date, .title, .character] | @tsv' "${tmpfile}" |
  while IFS=$'\t' 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

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

  echo "${output}"
}

#==============================================================================
# DBMovie_GetActorBloggerUrl: Return the Blogger URL for an actor by IMDb ID
#------------------------------------------------------------------------------
# Description:
#   Looks up the actor.db file and retrieves the Blogger URL associated with the
#   specified IMDb ID. The actor.db is a tab-separated file where the 7th field
#   stores the Blogger URL. If no match is found, the function returns nothing.
#
# Arguments:
#   ${1} - IMDb ID of the actor (e.g., nm0000493)
#
# Globals Used:
#   ACTOR_DB_PATH - Path to the actor.db file containing cached actor metadata
#
# Outputs:
#   - Echoes the Blogger URL to stdout if found
#   - Returns silently if no match is found or actor.db is missing
#
# Return:
#   0 - If the Blogger URL was found and printed
#   1 - If the IMDb ID is missing or no record exists
#==============================================================================
DBMovie_GetActorBloggerUrl() {
  local -r imdb_id="${1}"
  [[ -z "${imdb_id}" ]] && return 1

  [[ ! -f "${ACTOR_DB_PATH}" ]] && return 1

  awk -F'\t' -v id="${imdb_id}" 'NR > 1 && $1 == id { print $7 }' "${ACTOR_DB_PATH}"
}

#==============================================================================
# DBMovie_InitMovieCreditsDb: Initialize movie_credits.db if missing or empty
#------------------------------------------------------------------------------
# Description:
#   Ensures that the movie_credits.db file exists and contains the appropriate
#   header. If the file is missing or empty, it is created and the header line
#   is written. The expected format is a pipe-separated file:
#
#     MOVIE_ID|ACTOR_ID|ROLE
#
#   This file is used to cache actor-to-movie credit mappings retrieved from TMDb.
#
# Globals Used:
#   MOVIE_CREDITS_DB_PATH - Path to the movie credits cache file
#   DEBUG                 - If enabled, logs a debug message when file is created
#
# Outputs:
#   - Creates movie_credits.db with a header if not present
#   - Logs debug message if DEBUG is enabled
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_InitMovieCreditsDb() {
  mkdir -p "$(dirname "${MOVIE_CREDITS_DB_PATH}")"
  if [[ ! -s "${MOVIE_CREDITS_DB_PATH}" ]]; then
    echo "MOVIE_ID|ACTOR_ID|ROLE" > "${MOVIE_CREDITS_DB_PATH}"
    (( DEBUG )) && EchoD "[DBMovie_InitMovieCreditsDb] Created ${MOVIE_CREDITS_DB_PATH} with header"
  fi
}

#==============================================================================
# DBMovie_GetMovieCreditsDbPath: Return the full path to movie_credits.db
#------------------------------------------------------------------------------
# Description:
#   Echoes the absolute path to the movie credits database file, which is used
#   to store mappings between actor IDs and their movie roles. The file format
#   is pipe-separated (|) and includes the following fields:
#
#     MOVIE_ID|ACTOR_ID|ROLE
#
#   This function provides a central reference for consistent path usage.
#
# Globals Used:
#   MOVIE_CREDITS_DB_PATH - Path to the movie credits database file
#
# Outputs:
#   Echoes the path to stdout
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_GetMovieCreditsDbPath() {
  echo "${MOVIE_CREDITS_DB_PATH}"
}

#==============================================================================
# DBMovie_SaveMovieCredit: Append a movie credit record to movie_credits.db
#------------------------------------------------------------------------------
# Description:
#   Saves a new entry to the movie_credits.db file associating a movie ID with
#   an actor ID and their role in that film. Entries are tab-separated and stored
#   in the format:
#
#     MOVIE_ID<TAB>ACTOR_ID<TAB>ROLE
#
#   This function does not enforce uniqueness or deduplication. It is the caller's
#   responsibility to avoid inserting duplicate records for the same (movie_id, actor_id) pair.
#
# Usage:
#   DBMovie_SaveMovieCredit <movie_id> <actor_id> <role>
#
# Arguments:
#   ${1} - TMDb movie ID (e.g., 550)
#   ${2} - TMDb actor ID (e.g., 819)
#   ${3} - Character name / role (e.g., "Rocky Balboa")
#
# Globals Used:
#   MOVIE_CREDITS_DB_PATH - Path to movie credits cache file
#
# Outputs:
#   - Appends a single line to movie_credits.db
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_SaveMovieCredit() {
  local movie_id="${1}"
  local actor_id="${2}"
  local role="${3}"
  local -r file="${MOVIE_CREDITS_DB_PATH}"

  role="${role//[$'\t\r\n']}"  # Sanitize role

  echo -e "${movie_id}\t${actor_id}\t${role}" >> "${file}"
}

#==============================================================================
# DBMovie_LoadMovieCreditsByActor: Load all movie credits for a given TMDb actor ID
#------------------------------------------------------------------------------
# Description:
#   Retrieves all movie credit entries from movie_credits.db for the specified
#   TMDb actor ID. Each entry in the file is tab-separated and represents a
#   single role in a movie, with fields:
#
#     MOVIE_ID<TAB>ACTOR_ID<TAB>ROLE
#
#   This function filters the database and returns only those lines where the
#   actor ID matches. It returns a non-zero status code if no records are found.
#
# Arguments:
#   ${1} - TMDb actor ID (e.g., 819)
#
# Globals Used:
#   MOVIE_CREDITS_DB_PATH - Path to the movie credits database
#
# Outputs:
#   - Echoes matching tab-separated lines to stdout:
#       <MOVIE_ID>\t<ACTOR_ID>\t<ROLE>
#
# Return:
#   0 - If one or more matching credits are found
#   1 - If no matching credits exist or the file is missing
#==============================================================================
DBMovie_LoadMovieCreditsByActor() {
  local -r actor_id="${1}"
  local -r file="${MOVIE_CREDITS_DB_PATH}"
  grep -P "^\d+\t${actor_id}\t" "${file}" || return 1
}

#==============================================================================
# DBMovie_DeleteCreditsByActor: Remove all movie credit entries for a given actor
#------------------------------------------------------------------------------
# Description:
#   Deletes all entries in movie_credits.db that match the specified TMDb actor ID.
#   This function is useful for purging outdated or erroneous credit records.
#   The operation is performed by filtering all lines that do not match the actor ID
#   and writing the result back to the original file.
#
# Arguments:
#   ${1} - TMDb actor ID (e.g., 819)
#
# Globals Used:
#   MOVIE_CREDITS_DB_PATH - Path to the movie credits database
#
# Outputs:
#   - Overwrites movie_credits.db with all non-matching entries
#
# Return:
#   0 - Always (even if no matching entries were found)
#==============================================================================
DBMovie_DeleteCreditsByActor() {
  local -r actor_id="${1}"
  local -r file="${MOVIE_CREDITS_DB_PATH}"
  grep -v -P "^\d+\t${actor_id}\t" "${file}" > "${file}.tmp" || true
  mv "${file}.tmp" "${file}"
}

#==============================================================================
# DBMovie_InitMovieDb: Initialize movie.db if missing or empty
#------------------------------------------------------------------------------
# Description:
#   Ensures that the movie metadata cache file (`movie.db`) exists and contains
#   the appropriate header line. If the file is missing or empty, it is created
#   with the following pipe-separated column headers:
#
#     TMDB_ID|IMDB_ID|COUNTRY|TITLE_EN|TITLE_ORIG|RELEASE_DATE
#
#   This file is used to cache metadata for movies retrieved from TMDb.
#
# Globals Used:
#   MY_MDB_CACHE   - Directory in which the cache file is stored
#   MOVIE_DB_PATH  - Full path to the movie.db file
#   DEBUG          - If enabled, logs file creation
#
# 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 - Always
#==============================================================================
DBMovie_InitMovieDb() {
  mkdir -p "${MY_MDB_CACHE}"

  if [[ ! -s "${MOVIE_DB_PATH}" ]]; then
    echo "TMDB_ID|IMDB_ID|COUNTRY|TITLE_EN|TITLE_ORIG|RELEASE_DATE" > "${MOVIE_DB_PATH}"
    (( DEBUG )) && EchoD "[DBMovie_InitMovieDb] Created ${MOVIE_DB_PATH} with header"
  fi
}

#==============================================================================
# DBMovie_SaveMovieRecord: Overwrite or append a movie record in movie.db
#------------------------------------------------------------------------------
# Description:
#   Inserts or updates a movie record in the local movie metadata cache file
#   (movie.db). If an existing entry with the same TMDb ID is found, it is removed.
#   The function writes a sanitized pipe-separated line containing the following fields:
#
#     TMDB_ID|IMDB_ID|COUNTRY|TITLE_EN|TITLE_ORIG|RELEASE_DATE
#
#   This function does not handle the watched date field. It is intended for
#   storing static metadata only.
#
# Arguments:
#   ${1} - TMDb movie ID (e.g., 550)
#   ${2} - IMDb ID (e.g., tt0137523)
#   ${3} - ISO 3166-1 alpha-2 country code (e.g., US)
#   ${4} - English title
#   ${5} - Original title
#   ${6} - Release date (YYYY-MM-DD)
#
# Globals Used:
#   MOVIE_DB_PATH - Path to the movie.db metadata cache
#
# Outputs:
#   - Rewrites movie.db with the updated or new entry
#
# Return:
#   0 - Always
#==============================================================================
DBMovie_SaveMovieRecord() {
  local -r tmdb_id="${1}"
  local -r imdb_id="${2}"
  local -r country="${3}"
  local title_en="${4}"
  local title_orig="${5}"
  local -r release_date="${6}"

  # Sanitize input (remove tabs, newlines, pipes)
  title_en="${title_en//[$'\t\r\n|']}"
  title_orig="${title_orig//[$'\t\r\n|']}"
  local -r clean_country="${country//[$'\t\r\n|']}"
  local -r clean_release="${release_date//[$'\t\r\n|']}"

  # Remove any existing entry for this TMDb ID
  grep -v -P "^${tmdb_id}\|" "${MOVIE_DB_PATH}" > "${MOVIE_DB_PATH}.tmp" 2>/dev/null || true

  # Add new entry
  echo "${tmdb_id}|${imdb_id}|${clean_country}|${title_en}|${title_orig}|${clean_release}" >> "${MOVIE_DB_PATH}.tmp"

  # Replace file
  mv "${MOVIE_DB_PATH}.tmp" "${MOVIE_DB_PATH}"
}

#==============================================================================
# DBMovie_SaveMovieMetadata: Save or update a movie entry in movie.db
#------------------------------------------------------------------------------
# Description:
#   Adds or updates a movie record in ${MOVIE_DB_PATH}. If a record with the
#   same TMDb ID exists, it is overwritten. The final line contains:
#
#     TMDB_ID | IMDB_ID | COUNTRY | ENGLISH_TITLE | ORIGINAL_TITLE |
#     RELEASE_DATE | WATCHED_DATE | BLOGGER_URL | PHOTO_URL
#
#   BLOGGER_URL is stored without the domain prefix (e.g., just "2025/03/...").
#   PHOTO_URL is stored in full.
#
# Globals:
#   MOVIE_DB_PATH    - Path to ~/.MyMDBCache/movie.db
#
# 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, may be empty)
#   ${8} - Blogger URL (full, will be trimmed)
#   ${9} - Photo URL (optional, full)
#
# Outputs:
#   Writes to ${MOVIE_DB_PATH}
#   Logs debug info via EchoD
#==============================================================================
DBMovie_SaveMovieMetadata() {
  local -r tmdb_id="${1}"
  local -r imdb_id="${2}"
  local -r country="${3}"
  local -r english_title="${4}"
  local -r original_title="${5}"
  local -r release_date="${6}"
  local -r watched_date="${7}"
  local -r full_blogger_url="${8}"
  local -r photo_url="${9}"

  local -r movie_file="${MOVIE_DB_PATH}"
  local blogger_suffix="${full_blogger_url}"
  local -r domain_prefix="https://afberendsen.blogspot.com/"

  # Trim the Blogger domain prefix
  if [[ "${blogger_suffix}" == "${domain_prefix}"* ]]; then
    blogger_suffix="${blogger_suffix#${domain_prefix}}"
  fi

  mkdir -p "$(dirname "${movie_file}")"
  touch "${movie_file}"

  # Remove old record, if exists
  grep -v "^${tmdb_id}|" "${movie_file}" > "${movie_file}.tmp" || true

  # Append new line
  local -r SEP='|'
  echo "${tmdb_id}${SEP}${imdb_id}${SEP}${country}${SEP}${english_title}${SEP}${original_title}${SEP}${release_date}${SEP}${watched_date}${SEP}${blogger_suffix}${SEP}${photo_url}" >> "${movie_file}.tmp"

  mv "${movie_file}.tmp" "${movie_file}"
  EchoD "${FUNCNAME[0]}:${LINENO}:✅ Saved movie metadata for TMDb ID ${tmdb_id}"
}

#==============================================================================
# DBMovie_HasMovieMetadata - Checks if a movie (by TMDb ID) exists in movie.db
# Usage   : DBMovie_HasMovieMetadata <tmdb_movie_id>
# Returns : 0 if found, 1 otherwise
#==============================================================================
DBMovie_HasMovieMetadata() {
  local -r tmdb_id="$1"
  grep -q -P "^${tmdb_id}\t" "${MOVIE_DB_PATH}" 2>/dev/null
}

#==============================================================================
# DBMovie_DeduplicateMovieDb - Cleans up duplicate TMDb or IMDb entries in movie.db
# Keeps only the last occurrence for any TMDb or IMDb ID
#==============================================================================
DBMovie_DeduplicateMovieDb() {
  local -r file="${MOVIE_DB_PATH}"
  local -r tmpfile="${file}.dedup.tmp"

  if [[ ! -f "${file}" ]]; then
    EchoE "❌ [DBMovie_DeduplicateMovieDb] Movie DB file not found: ${file}"
    return 1
  fi

  Echo "🧹 Deduplicating movie.db at ${file}..."

  # Preserve header
  head -n 1 "${file}" > "${tmpfile}"

  # Deduplicate by keeping only the last occurrence of TMDb or IMDb ID
  tail -n +2 "${file}" | tac | awk -F'\t' '
    {
      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}"

  mv "${tmpfile}" "${file}"
  Echo "✅ movie.db has been deduplicated successfully."
}

#==============================================================================
# DBMovie_GetMovieWatchedDate: Retrieve watch date from movie.db for a given TMDb ID
#------------------------------------------------------------------------------
# Description:
#   Looks up the watch date field in movie.db based on TMDb movie ID.
#   Assumes the watch date is in column 7. If the movie or the field is missing,
#   an empty string is returned.
#
# Globals Used:
#   MOVIE_DB_PATH           - Path to movie.db
#
# Arguments:
#   $1 - TMDb movie ID
#
# Outputs:
#   Echoes the watched date (YYYY-MM-DD) or nothing if not found.
#==============================================================================
DBMovie_GetMovieWatchedDate() {
  local -r movie_id="${1}"
  grep -P "^${movie_id}\t" "${MOVIE_DB_PATH}" | awk -F '\t' '{ print $7 }'
}

#==============================================================================
# MarkMovieAsWatched: Update WATCHED_DATE field in movie.db for a given TMDb ID
#------------------------------------------------------------------------------
# Description:
#   Updates the watch date field for a specific movie in the movie.db file. If
#   the movie exists, it replaces the 7th column with the given date (or today
#   if not specified). Preserves all other fields. A temporary file is used to
#   ensure atomic writes.
#
# Globals Used:
#   MOVIE_DB_PATH           - Path to movie.db file
#
# Arguments:
#   $1 - TMDb movie ID
#   $2 - [optional] Watched date (YYYY-MM-DD). If omitted, uses today's date.
#
# Outputs:
#   Updates movie.db in place.
#==============================================================================
MarkMovieAsWatched() {
  local -r movie_id="${1}"
  local -r date="${2:-$(date +%F)}"
  awk -F '\t' -v id="${movie_id}" -v d="${date}" 'BEGIN {OFS=FS} {
    if ($1 == id) $7 = d;
    print
  }' "${MOVIE_DB_PATH}" > "${MOVIE_DB_PATH}.tmp" && mv "${MOVIE_DB_PATH}.tmp" "${MOVIE_DB_PATH}"
}

#==============================================================================
# DBMovie_LoadMoviesByActor: Return movie.db entries by TMDb actor ID
#------------------------------------------------------------------------------
# Description:
#   Outputs all movie.db lines corresponding to a given TMDb actor ID by
#   looking up matching entries from movie_credits.db. This ensures tab-aligned,
#   complete movie.db lines are returned (including new fields like blogger_url).
#
# Globals:
#   MOVIE_DB_PATH
#   MOVIE_CREDITS_DB_PATH
#
# Arguments:
#   $1 - TMDb actor ID (required)
#
# Outputs:
#   Full lines from movie.db (tab-separated) for the given actor.
#
#==============================================================================
DBMovie_LoadMoviesByActor() {
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: $*"
  local -r actor_tmdb_id="${1}"

  if [[ -z "${actor_tmdb_id}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Missing TMDb actor ID"
    return 1
  elif [[ ! -s "${MOVIE_CREDITS_DB_PATH}" || ! -s "${MOVIE_DB_PATH}" ]]; then
    EchoE "${FUNCNAME[0]}:${LINENO}: Required DB file missing"
    return 1
  fi

  # Extract movie IDs once
  local tmp_ids
  tmp_ids="$(mktemp)"
  awk -F'\t' -v id="${actor_tmdb_id}" '$2 == id { print $1 }' "${MOVIE_CREDITS_DB_PATH}" > "${tmp_ids}"

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

  # Match full lines in movie.db once
  grep -F -f "${tmp_ids}" "${MOVIE_DB_PATH}" | grep -E '^[0-9]+\|'
  rm -f "${tmp_ids}"
}

#==============================================================================
# DBMovie_GetMovieRole: Retrieve actor's role for a given movie from cache
#------------------------------------------------------------------------------
# Description:
#   Extracts the role name an actor played in a specific movie, based on the
#   combination of TMDb actor ID and TMDb movie ID. The function searches
#   movie_credits.db and returns the role string. If no role is found, it
#   returns an empty string.
#
# Globals:
#   MY_MDB_CACHE      - Base path to local metadata cache directory
#   FUNCNAME          - Used in logging/debug output
#
# Arguments:
#   ${1} - TMDb Movie ID (e.g., 239)
#   ${2} - TMDb Actor ID (e.g., 3151)
#
# Returns:
#   Echoes role string on FD 5 (e.g., "Joe / Josephine / Junior")
#   Writes debug message if role is found
#==============================================================================
DBMovie_GetMovieRole() {
  local -r movie_id="${1}"
  local -r actor_id="${2}"
  local -r credits_file="${MY_MDB_CACHE}/movie_credits.db"

  local line role

  if [[ ! -s "${credits_file}" ]]; then
    EchoD "${FUNCNAME[0]}:${LINENO}: movie_credits.db is missing or empty"
    return 1
  fi

  # Match line by both movie ID and actor ID
  while IFS=$'\t' read -r m_id a_id r; do
    if [[ "${m_id}" == "${movie_id}" && "${a_id}" == "${actor_id}" ]]; then
      role="${r}"
      break
    fi
  done < "${credits_file}"

  if [[ -n "${role}" ]]; then
    (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Found role: '${role}' for movie_id=${movie_id}, actor_id=${actor_id}"
    echo "${role}"
  else
    echo ""
  fi
}

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

#==============================================================================
# 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}'"
}

#==============================================================================
# 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 "WARNING=> ⚠️ 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
}

#==============================================================================
# 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

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
}

#==============================================================================
# GetActorMappingPath: Return the path to the legacy actor-to-blog mapping file
#------------------------------------------------------------------------------
# Description:
#   Returns the full path to the file used for mapping IMDb actor IDs to
#   Blogger post URL suffixes. This was the original format before the adoption
#   of actor.db. The file is typically located under ~/.MyBlogCache.
#
#   This function is retained for backward compatibility with older scripts or
#   migration tooling. The preferred method now uses actor.db instead.
#
# Globals Used:
#   HOME - The user’s home directory (used to construct the path)
#
# Outputs:
#   - Echoes the full file path: ~/.MyBlogCache/actor_posts.map
#
# Return:
#   0 - Always
#==============================================================================
GetActorMappingPath() {
  echo "${HOME}/.MyBlogCache/actor_posts.map"
}

#==============================================================================
# LoadActorPostUrl: Retrieve Blogger URL suffix for a given IMDb actor ID
#------------------------------------------------------------------------------
# Description:
#   Looks up the legacy actor-to-blog mapping file (actor_posts.map) to find
#   the Blogger URL suffix associated with the given IMDb ID. Each line in the
#   mapping file is expected to be in the format:
#
#     IMDB_ID|blogger-url-suffix
#
#   This function is part of legacy support and is typically replaced by newer
#   database access via actor.db.
#
# Arguments:
#   ${1} - IMDb actor ID (e.g., nm0000493)
#
# Globals Used:
#   HOME - Used to determine the mapping file path
#
# Outputs:
#   - Echoes the matching Blogger URL suffix (without base domain) to stdout
#
# Return:
#   0 - Success (match found)
#   1 - Failure (file missing or no match found)
#==============================================================================
LoadActorPostUrl() {
  local -r imdb_id="${1}"
  local -r file_path="$(GetActorMappingPath)"
  [[ ! -f "${file_path}" ]] && return 1
  grep -E "^${imdb_id}\|" "${file_path}" | cut -d'|' -f2-
}

#==============================================================================
# SaveActorPostUrl: Save or update the Blogger URL suffix for a given IMDb ID
#------------------------------------------------------------------------------
# Description:
#   Writes or updates the mapping between an IMDb actor ID and a Blogger URL
#   suffix in the legacy actor mapping file `actor_posts.map`. If an existing
#   entry for the IMDb ID exists, it is removed before appending the new one.
#
#   This function maintains backward compatibility for systems that use the
#   older `actor_posts.map` format (before the adoption of actor.db).
#
# Arguments:
#   ${1} - IMDb ID (e.g., nm0000493)
#   ${2} - Blogger URL suffix (e.g., 2025/04/actor-jack-lemmon-1925-2001)
#
# Globals Used:
#   HOME - Used to derive the mapping file path
#
# Outputs:
#   - Overwrites or appends a line in ~/.MyBlogCache/actor_posts.map
#
# Return:
#   0 - Always
#==============================================================================
SaveActorPostUrl() {
  local -r imdb_id="${1}"
  local -r path_suffix="${2}"
  local -r file_path="$(GetActorMappingPath)"
  mkdir -p "$(dirname "${file_path}")"
  # Remove previous entry if any
  grep -v -E "^${imdb_id}\|" "${file_path}" > "${file_path}.tmp" 2>/dev/null || true
  echo "${imdb_id}|${path_suffix}" >> "${file_path}.tmp"
  mv "${file_path}.tmp" "${file_path}"
}

#==============================================================================
# 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 "⚠️ WARNING: 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: Top-level wrapper to generate and display actor filmography
#------------------------------------------------------------------------------
# Description:
#   High-level driver that coordinates all steps required to generate the
#   HTML-formatted actor filmography post. This function:
#
#     - Initializes caches
#     - Loads or fetches actor metadata
#     - Loads or downloads movie credits
#     - Retrieves per-movie metadata
#     - Formats the final HTML output with photo, links, biography, and table
#
#   The resulting HTML block is exported via the global `ACTOR_FILMOGRAPHY`
#   variable. This function is typically invoked from within blog update logic.
#
# Globals Used:
#   ACTOR_IMDB_ID       - IMDb ID of the actor (input)
#   ACTOR_QUERY         - Fallback actor name (input)
#   TMDB_API_KEY        - API key for TMDb access
#   MY_MDB_CACHE        - Base directory for local caches
#   DEBUG, VERBOSE      - Logging flags
#
# Globals Set:
#   ACTOR_NAME, ACTOR_DOB, ACTOR_DOD, ACTOR_PHOTO_URL,
#   ACTOR_BLOG_URL, ACTOR_BIRTH_YEAR, ACTOR_TMDB_ID
#   ACTOR_FILMOGRAPHY   - Final HTML output block
#
# Outputs:
#   - Echoes progress and diagnostics depending on VERBOSE and DEBUG flags
#   - Populates ACTOR_FILMOGRAPHY with full blog-compatible HTML
#
# Return:
#   0 - Success
#   1 - Any failure (API fetch error, cache corruption, missing keys)
#==============================================================================
GenerateActorFilmography() {
  local -r actor_name="${1}"
  local actor_id=""
  local birth_year=""
  local html=""
  local tmpfile
  tmpfile=$(mktemp)

  Echo "Looking up TMDb ID for '${actor_name}'..."

  actor_id=$(curl --silent --get "https://api.themoviedb.org/3/search/person" \
    --data-urlencode "api_key=${TMDB_API_KEY}" \
    --data-urlencode "query=${actor_name}" |
    jq -r '.results[0].id')

  if [[ -z "${actor_id}" || "${actor_id}" == "null" ]]; then
    EchoE "ERROR: Could not find TMDb ID for '${actor_name}'."
    return 1
  fi

  birth_year=$(curl --silent --get "https://api.themoviedb.org/3/person/${actor_id}" \
    --data-urlencode "api_key=${TMDB_API_KEY}" |
    jq -r '.birthday' | cut -d'-' -f1)

  Echo "Fetching movie credits for '${actor_name}'..."

  curl --silent --get "https://api.themoviedb.org/3/person/${actor_id}/movie_credits" \
    --data-urlencode "api_key=${TMDB_API_KEY}" > "${tmpfile}"

  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>'

  jq -r '.cast[] | [.release_date, .title, .character, .id] | @tsv' "${tmpfile}" |
  while IFS=$'\t' read -r release_date title role tmdb_movie_id; do
    [[ -z "${release_date}" || -z "${title}" ]] && continue

    local release_year="${release_date:0:4}"
    local imdb_id=""
    local age="?"

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

    imdb_id=$(curl --silent "https://api.themoviedb.org/3/movie/${tmdb_movie_id}/external_ids?api_key=${TMDB_API_KEY}" |
      jq -r '.imdb_id // empty')

    local safe_role=$(echo "${role}" | sed -E 's/&/&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
    local imdb_link="${title}"
    if [[ -n "${imdb_id}" && "${imdb_id}" != "null" ]]; then
      imdb_link="<a href=\"https://www.imdb.com/title/${imdb_id}\" target=\"_blank\">${title}</a>"
    fi

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

    html+="<tr><td>${release_date}</td><td>${imdb_link}</td><td>${safe_role}</td><td>${age}</td><td>&#x2713;</td></tr>"
  done

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

  echo "${html}"
}

#==============================================================================
# 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() {
  local -r actor_name="${1}"
  local -r tmdb_id="$(GetActorTmdbId "${actor_name}")"

  if [[ -z "${tmdb_id}" || "${tmdb_id}" == "null" ]]; then
    EchoE "❌ Could not find TMDb ID for '${actor_name}'."
    return 1
  fi

  local -r url="https://api.themoviedb.org/3/person/${tmdb_id}/external_ids?api_key=${TMDB_API_KEY}"
  local imdb_id
  imdb_id=$(curl --silent --fail "${url}" | jq -r '.imdb_id')

  if [[ -z "${imdb_id}" || "${imdb_id}" == "null" ]]; then
    EchoE "❌ Failed to resolve IMDb ID from TMDb ID ${tmdb_id}."
    return 1
  fi

  echo "${imdb_id}"
}

#==============================================================================
# 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 )) && EchoD "[${FUNCNAME[0]}] 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
}

#==============================================================================
# GetActorTmdbId: Resolve TMDb person ID from actor name
#------------------------------------------------------------------------------
# Description:
#   Queries the TMDb search endpoint with the given actor name to retrieve the
#   corresponding TMDb person ID. The query string is URL-encoded. This function
#   is typically used as the first step in resolving an actor's metadata from TMDb.
#
# Arguments:
#   ${1} - Actor name (e.g., "Jack Lemmon")
#
# Globals Used:
#   TMDB_API_KEY     - Required TMDb API key
#
# Outputs:
#   Echoes the numeric TMDb ID to stdout if found
#
# Return:
#   0 - Success (ID found and printed)
#   1 - Failure (ID not found or TMDb error)
#==============================================================================
GetActorTmdbId() {
  local -r actor_name="${1}"
  local url="https://api.themoviedb.org/3/search/person?query=$(urlencode "${actor_name}")&api_key=${TMDB_API_KEY}"
  local tmdb_id

  tmdb_id=$(curl --silent --fail "${url}" | jq -r '.results[0].id')

  if [[ "${tmdb_id}" == "null" || -z "${tmdb_id}" ]]; then
    return 1
  fi

  echo "${tmdb_id}"
}

#==============================================================================
# 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

  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) PrintHelp; exit 0 ;;
      -V|--version) Echo "Version: ${SCRIPT_VERSION}"; exit 0 ;;
      -n|--dry-run) DRYRUN=1 ; shift ;;
      -v|--verbose) ((VERBOSE++)) ; shift ;;
      -D|--debug) DEBUG=1 ; shift ;;
      -d|--date) CUSTOM_DATE="${2}" ; shift 2 ;;

      --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 ;;
      --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
        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
        ;;
      --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
        ;;
      --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 mini_bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db"

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

  local imdb_bio_url="https://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm"
  local tmp_bio_html
  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

  local links_file="${MY_MDB_CACHE}/links.db"
  mkdir -p "$(dirname "${links_file}")"
  if ! grep -q "^${imdb_id}[[:space:]]+.*#mini_bio" "${links_file}" 2>/dev/null; then
    echo -e "${imdb_id}\thttps://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm#mini_bio" >> "${links_file}"
    EchoD "${FUNCNAME[0]}:${LINENO}:🔗 Added IMDb Mini Bio link to ${links_file}"
  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 ~/.MyBlogCache/*.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 Used:
#   ACTOR_IMDB_ID           - IMDb ID of the actor (required)
#   ACTOR_QUERY             - Actor name for fallback messaging
#   ACTOR_NAME              - Actor name (exported here)
#   ACTOR_DOB               - Date of birth, YYYY-MM-DD (exported here)
#   ACTOR_DOD               - Date of death, if any (exported here)
#   ACTOR_BIRTH_YEAR        - Year of birth as integer (exported here)
#   ACTOR_PHOTO_URL         - Full TMDb profile image URL (exported here)
#   ACTOR_BLOG_URL          - Cached Blogger post URL (exported here)
#   ACTOR_TMDB_ID           - TMDb actor ID (set internally)
#   ACTOR_FILMOGRAPHY       - HTML output block (set by this function)
#   TMDB_API_KEY            - TMDb API key
#   MY_MDB_CACHE            - Path to ~/.MyBlogCache
#   DRYRUN                  - If set, avoid writing to any cache
#   VERBOSE                 - Controls debug/info output
#
# Outputs:
#   Populates 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
#
# 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 role release title year age age_cell imdb_link tmdb_link watched_date blogger_url photo_url

  DBMovie_InitActorDb
  DBMovie_InitMovieCreditsDb
  DBMovie_InitMovieDb

  record=$(DBMovie_LoadActorRecord "${imdb_id}")
  if [[ -n "${record}" ]]; then
    IFS=$'\t' read -r _ tmdb_id name dob dod photo_url blog_url <<< "${record}"
    export ACTOR_NAME="${name}"
    export ACTOR_DOB="${dob}"
    export ACTOR_DOD="${dod}"
    export ACTOR_PHOTO_URL="${photo_url}"
    export ACTOR_BLOG_URL="${blog_url}"
    export ACTOR_BIRTH_YEAR="${dob:0:4}"
    ACTOR_TMDB_ID="${tmdb_id}"
    Echo "📇 Loaded actor from cache: ${ACTOR_NAME} (${dob}–${dod})"
  else
    EchoW "🔍 No match found in local cache. Fetching data for '${ACTOR_NAME}' from TMDb..."

    tmdb_id=$(curl --silent --fail "https://api.themoviedb.org/3/find/${imdb_id}?api_key=${TMDB_API_KEY}&external_source=imdb_id" | jq -r '.person_results[0].id')

    if [[ -z "${tmdb_id}" || "${tmdb_id}" == "null" ]]; then
      EchoE "Could not find TMDb ID for '${imdb_id}'."
      return 1
    fi

    details=$(curl --silent --fail "https://api.themoviedb.org/3/person/${tmdb_id}?api_key=${TMDB_API_KEY}")

    name=$(jq -r '.name' <<< "${details}")
    dob=$(jq -r '.birthday // empty' <<< "${details}")
    dod=$(jq -r '.deathday // empty' <<< "${details}")
    photo_url=$(jq -r '.profile_path // empty' <<< "${details}")
    [[ -n "${photo_url}" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"

    DBMovie_SaveActorRecord "${imdb_id}" "${tmdb_id}" "${name}" "${dob}" "${dod}" "${photo_url}" ""
    
    # IMDb Mini Bio: download and extract the main paragraph
    local imdb_bio_url="https://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm"
    local mini_bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db"
    local tmp_bio_html

    tmp_bio_html="$(mktemp)"

    curl --silent --fail "${imdb_bio_url}" > "${tmp_bio_html}"

    # Try using pup (HTML parser), fallback to grep/sed
    if command -v pup >/dev/null 2>&1; then
      pup 'h4:contains("Mini Bio") + .soda text{}' < "${tmp_bio_html}" | \
        sed '/^[[:space:]]*$/d' > "${mini_bio_file}"
    else
      sed -n '/Mini Bio/,/<h4 class="li_group">/p' "${tmp_bio_html}" |
        sed -n '/<div class="soda[^>]*">/,/<\/div>/p' > "${mini_bio_file}"
    fi

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

    rm -f "${tmp_bio_html}"

    # Append Mini Bio permalink to links.db
    local links_file="${MY_MDB_CACHE}/links.db"
    mkdir -p "$(dirname "${links_file}")"
    if ! grep -q "^${imdb_id}[[:space:]]+.*#mini_bio" "${links_file}" 2>/dev/null; then
      echo -e "${imdb_id}\thttps://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm#mini_bio" >> "${links_file}"
      EchoD "${FUNCNAME[0]}:${LINENO}:🔗 Added IMDb Mini Bio link to ${links_file}"
    fi

    EchoD "🆕 Added '${name}' to cache (IMDb: ${imdb_id})"

    export ACTOR_NAME="${name}"
    export ACTOR_DOB="${dob}"
    export ACTOR_DOD="${dod}"
    export ACTOR_PHOTO_URL="${photo_url}"
    export ACTOR_BLOG_URL=""
    export ACTOR_BIRTH_YEAR="${dob:0:4}"
    ACTOR_TMDB_ID="${tmdb_id}"
  fi
  
  EnsureActorMiniBioCached "${imdb_id}"

  credits_cached=$(DBMovie_LoadMovieCreditsByActor "${ACTOR_TMDB_ID}") || credits_cached=""

  if [[ -z "${credits_cached}" ]]; then
    EchoW "📥 No credits found in local cache for TMDb actor ID ${ACTOR_TMDB_ID}, fetching..."
    credits_json=$(curl --silent --fail \
      "https://api.themoviedb.org/3/person/${ACTOR_TMDB_ID}/movie_credits?api_key=${TMDB_API_KEY}")

    if [[ -z "${credits_json}" ]]; then
      EchoE "Failed to fetch credits from TMDb."
      return 1
    fi

    downloaded_count=0
    echo "${credits_json}" |
    jq -r '[.cast[] | select(.release_date != null and .title != null and .character != null)] |
            sort_by(.release_date)[] |
            [.id, .title, .character] | @tsv' |
    while IFS=$'\t' read -r movie_id title role; do
      DBMovie_SaveMovieCredit "${movie_id}" "${ACTOR_TMDB_ID}" "${role}"
      (( downloaded_count++ ))
    done

    (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}:📦 Downloaded and cached ${downloaded_count} filmography entries for ${ACTOR_NAME}."
    credits_cached=$(DBMovie_LoadMovieCreditsByActor "${ACTOR_TMDB_ID}") || {
      EchoE "Failed to read back credits after saving."
      return 1
    }
  else
    cached_lines=$(echo "${credits_cached}" | wc -l)
    Echo "✅ Using cached movie credits for TMDb ID ${ACTOR_TMDB_ID} (${cached_lines} records)"
  fi

  output=""
  (( 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>"

  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

  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 < <(DBMovie_LoadMoviesByActor "${ACTOR_TMDB_ID}" | sort -t'|' -k6)
  
  # end of filmography table
  output+="</tbody></table>"

  # begining 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
  local links_file="${MY_MDB_CACHE}/links.db"
  if [[ -f "${links_file}" ]]; then
    while read -r dummy url; do
      output+="<li><a href=\"${url}\" target=\"_blank\">${url}</a></li>"
    done < <(grep -E "^${imdb_id}[[:space:]]+" "${links_file}" | sort -u)
  else
    EchoW "No links.db file found at ${links_file}"
  fi
  output+="</ul>"

  output+="<h2>Bio</h2>"
  local bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db"
  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 full flow of building, previewing, or submitting
#   a Blogger blog post for an actor’s filmography. If a Blogger URL already
#   exists and update mode is not enabled, the user is prompted to:
#     (a) copy the HTML to clipboard,
#     (b) save it to a local HTML file,
#     (c) publish a new Blogger post and update the actor cache, or
#     (d) cancel the operation.
#   Otherwise, it regenerates and submits a new draft directly.
#
# Globals Used:
#   ACTOR_IMDB_ID         - IMDb ID of the actor
#   ACTOR_QUERY           - Name of the actor (used for fallback warning)
#   ACTOR_FILMOGRAPHY     - HTML content generated by GenerateActorFilmographyFromTmdb
#   UPDATE_MODE           - Set to 1 if forcing Blogger update
#   DEBUG                 - If non-zero, enable debug output
#   DRYRUN                - If set, suppress actual blog submission
#
# Outputs:
#   - Echoes progress and user options
#   - Optionally writes filmography HTML to clipboard or file
#   - Submits a Blogger draft (unless DRYRUN is enabled)
#   - Updates actor.db with the Blogger URL if published
#
# Exit Codes:
#   0 - Success
#   1 - Failure or user cancellation
#==============================================================================
DoActor_0_Filmography() {
  local new_url
  local blogger_url

  Echo "Generating filmography for: ${actor_name}"
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Using IMDb ID: ${ACTOR_IMDB_ID}"

  [[ -z "${ACTOR_IMDB_ID}" ]] && EchoE "Could not resolve IMDb ID for actor: ${actor_name}" && exit 1

  blogger_url="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")"

  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 /dev/clipboard"
      Echo "  b) Save to file: ${ACTOR_IMDB_ID}.html"
      Echo "  c) Create a new Blogger entry and update cache"
      Echo "  d) Cancel"
      read -rp "Your choice (a/b/c/d): " choice
      choice="${choice:0:1}"
      choice="${choice,,}"

      case "${choice,,}" in
        a)
          GenerateActorFilmographyFromTmdb
          [[ -z "${ACTOR_FILMOGRAPHY}" ]] && EchoE "Failed to generate filmography." && exit 1
          echo "${ACTOR_FILMOGRAPHY}" > /dev/clipboard
          Echo "Copied HTML to /dev/clipboard."
          exit 0
          ;;
        b)
          GenerateActorFilmographyFromTmdb
          [[ -z "${ACTOR_FILMOGRAPHY}" ]] && EchoE "Failed to generate filmography." && exit 1
          echo "${ACTOR_FILMOGRAPHY}" > "${ACTOR_IMDB_ID}.html"
          Echo "Saved HTML to ${ACTOR_IMDB_ID}.html"
          exit 0
          ;;
        c)
          GenerateActorFilmographyFromTmdb
          [[ -z "${ACTOR_FILMOGRAPHY}" ]] && EchoE "Failed to generate filmography." && exit 1
          new_url="$(SubmitBlogDraft "${ACTOR_FILMOGRAPHY}")"
          Echo "🌐 Blogger URL: ${new_url}"
          DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_url#*blogspot.com/}"
          break
          ;;
        d)
          Echo "Aborted by user."
          exit 1
          ;;
      esac
      EchoE "Invalid choice '${choice}'. Try again."
    done
  else
    # No existing Blogger post or in update mode
    GenerateActorFilmographyFromTmdb
    [[ -z "${ACTOR_FILMOGRAPHY}" ]] && EchoE "Failed to generate filmography." && exit 1
    new_url="$(SubmitBlogDraft "${ACTOR_FILMOGRAPHY}")"
    Echo "🌐 Blogger URL: ${new_url}"
    DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_url#*blogspot.com/}"
  fi

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

#==============================================================================
# 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: 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]}: 1='${1}'  2='${2}'  3='${3}'"
  local -r mode="${1:-0}"

  DBMovie_InitActorDb
  DBMovie_InitMovieDb
  DBMovie_InitMovieCreditsDb

  [[ -n "${ACTOR_QUERY}" && -n "${IMDB_ID}" ]] && ACTOR_QUERY=""
  local actor_name="${ACTOR_QUERY}"
  ACTOR_IMDB_ID="$(ResolveActorImdbId "${actor_name}")"

  [[ -z "${ACTOR_IMDB_ID}" ]] && EchoE "Could not resolve IMDb ID for actor: ${actor_name}" && exit 1

  local record="$(DBMovie_LoadActorRecord "${ACTOR_IMDB_ID}")"
  [[ -z "${record}" ]] && EchoE "Actor not found in actor.db: ${actor_name}" && exit 1

  case "${mode}" in
    0) DoActor_0_Filmography    "${2}" ;;
    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}" ;;
    *) EchoE "${FUNCNAME[0]}: Invalid execution mode '${mode}'" ; exit 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
  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() {
  DoDependency
  ParseArguments "$@"
  case ${EXECUTION_MODE} in
    1) RunMovieUpdate ;;  # Movie list mode
    2) case ${EXECUTION_SUBMODE} in
       # 0: list actor filmography
       # 1: show actor blogger url
       # 2: delete actor blogger url
       # 3: update actor blogger url
       # 4: revert blogger url
       # 5: list blogger url
       0|1|2|5) DoActor ${EXECUTION_SUBMODE} ;;
       3)       DoActor ${EXECUTION_SUBMODE} "${NEW_BLOGGER_URL}" ;;
       4)       DoActor ${EXECUTION_SUBMODE} "${REVERT_STEPS:-1}" ;;
       *)       EchoE "Invalid --actor execution mode ${EXECUTION_SUBMODE}" ; exit 1 ;;
       esac
       ;;
    *) EchoE "Invalid execution mode" ; exit 1 ;;
  esac
}

Main "$@" 
exit 0

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

Comments

Popular posts from this blog

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

 

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

 

Movies - Deadpool & Wolverine (2024)