#!/usr/bin/env bash
[[ "$(uname -a)" != *CYGWIN_NT* ]] && echo "This script can only be executed under Cygwin" && exit 1
# 🧠 ChatGPT-aware developer instructions at bottom of script (__CHATGPT_INSTRUCTIONS_BEGIN__)
################################################################################
# Script Name: MovieUpdateDraft.sh
# Description: Scans movie directories for files updated on a specific date,
# grouped by year, and submits a DRAFT post to Blogger with a table of changes.
#
# Version History:
# v1.0 - 2025-04-16 - Initial version
# v1.1 - 2025-04-16 - Fixed Blogger API JSON parse error (HTML table incorrectly escaped)
# v1.2 - 2025-04-16 - Replaced literal '\n' with <br> in HTML content
# v1.3 - 2025-04-16 - Removed empty <li> element from bullet list
# v1.4 - 2025-04-16 - Added intro paragraph above the table describing update date
# v1.5 - 2025-04-16 - Formatted table header (blue background, white bold font)
# v1.6 - 2025-04-16 - Inserted HTML comment identifying generating script
# v1.7 - 2025-04-16 - Fixed invalid "REFERENCE_DATE" parsing bug (date CLI error)
# v1.8 - 2025-04-16 - Replaced "(updated REFERENCE_DATE)" with actual date text "(updated YYYY-MM-DD)"
# v1.9 - 2025-04-16 - Added support for --date and --debug switches
# v1.10 - 2025-04-16 - Adjusted verbosity logic for no-match and matched file output
# v1.11 - 2025-04-16 - Controlled "Checking title" debug echo using DEBUG flag
# v1.12 - 2025-04-16 - Sorted matched titles alphabetically within each year group
# v1.13 - 2025-04-16 - Added distinction between "added" vs. "changed" titles (based on presence of metadata.json)
# v1.14 - 2025-04-16 - Separated logic for new/changed titles into HTML output structure
# v1.15 - 2025-04-16 - Improved OMDb cache initialization and format validation
# v1.16 - 2025-04-16 - Implemented IMDb runtime and year consistency checks
# v1.17 - 2025-04-16 - Added collapsible file list support for each title row
# v1.18 - 2025-04-16 - Added support for multiple years using --year (cumulative)
# v1.19 - 2025-04-16 - Introduced optional HTML export toggle with --no-html
# v1.20 - 2025-04-16 - Appended update type (Added vs Changed) as suffix in bullet list
# v1.21 - 2025-04-16 - Finalized consistent year traversal and grouping logic
# v1.22 - 2025-04-16 - Optimised local OMDb metadata caching with minimal lookups
# Version History:
# v2.0 - 2025-04-16 - 🟦 Major release: integrated OMDb API, poster images, runtime, categories, collapsible UI, HTML export, Blogger DRAFT formatting, and comprehensive update type detection
# v2.1 - 2025-04-16 - Added --year (-y) and --range (-r) options to limit processing by year or year range
# v2.2 - 2025-04-16 - Changed YESTERDAY to REFERENCE_DATE throughout for clarity and accuracy
# v2.3 - 2025-04-16 - Added --publish flag to enable automatic scheduling as LIVE post
# v2.4 - 2025-04-16 - Added retry logic for transient curl DNS failures (curl error 6: Could not resolve host)
# v2.5 - 2025-04-16 - Fixed UTC scheduling issue by posting at 21:59 UTC to appear as 23:59 local
# v2.6 - 2025-04-16 - Added support for --title (-t) to override the default post title, and --label (-l) to add one or more Blogger labels
# v2.7 - 2025-04-16 - Added --force (-f) to skip confirmation warning when using --publish with filtered years
# v2.8 - 2025-04-16 - Token retrieval now retries indefinitely on failure with 2s backoff and visual feedback
# v2.9 - 2025-04-16 - Fixed logic bug in ScanDirectories(): OMDb was being called outside of loop context
# v2.10 - 2025-04-16 - Optimized OMDb API calls: lookup now occurs only for matched folders with updates
# v2.11 - 2025-04-16 - Appended OMDb genres in bold purple [Genre] to each title entry in the post (within <li>)
# v2.12 - 2025-04-16 - Added clickable IMDb links for each title using OMDb imdbID metadata
# v2.13 - 2025-04-16 - Appended runtime (e.g. 103 min) to genre tag for each title using OMDb metadata
# v2.14 - 2025-04-16 - Integrated TMDb actor lookup: new GetTopActorsFromTmdb() with name, role, and age (based on release year)
# v2.15 - 2025-04-16 - Introduced collapsible cast block using <details> + <summary> for cleaner blog display
# v2.16 - 2025-04-16 - Cast block supports up to 7 actors; age calculation based on release year minus birth year
# v2.17 - 2025-04-16 - Fixed bug: all Echo* output now routed via FD 10/11 to prevent HTML output contamination
# v2.18 - 2025-04-16 - TMDb key is now validated automatically at script start; --force-actors overrides failures
# v2.19 - 2025-04-16 - Blog post generation now correctly sorts titles alphabetically within each year
# v2.20 - 2025-04-16 - Fixed multibyte sort bug (e.g., emojis in <summary>) using LC_ALL=C
# v2.21 - 2025-04-19 - Refactored --check-deps to support default, local-only, and install modes
#
# Version History:
# v3.0 - 2025-04-19 - 🎭 Major release: implemented --actor mode for generating actor blog posts using TMDb/OMDb metadata
# v3.1 - 2025-04-19 - Added support for loading actor metadata from actor.db; fallback to TMDb if not cached
# v3.2 - 2025-04-20 - Implemented movie credits caching via movie_credits.db and chronological role sorting
# v3.3 - 2025-04-20 - Appended IMDb and TMDb movie links to filmography entries; fallback for missing codes as N/A
# v3.4 - 2025-04-21 - Introduced movie.db with support for title, release date, original title, and country
# v3.5 - 2025-04-21 - Added support for external link sections via links.db and biography via cached HTML
# v3.6 - 2025-04-22 - Added WATCHED_DATE field to movie.db; Watch column now displays ✅ icon if set
# v3.7 - 2025-04-22 - Introduced DBMovie_GetMovieWatchedDate; improved fallback logic for empty IMDb/TMDb codes
# v3.8 - 2025-04-22 - Standardized headers across all DBMovie_* and DoActor_* functions; updated SaveMovieMetadata to support optional watched date
# v3.9 - 2025-04-22 - Added MarkMovieAsWatched() helper function to update watch status in movie.db by TMDb ID
#
# Version History:
# v4.0 - 2025-04-23 - 🎬 Extended movie.db format with BLOGGER_URL and PHOTO_URL fields; DBMovie_SaveMovieMetadata updated accordingly
#==============================================================================
# To do:
# - at exit, remove all possible temp files
#
# Now, an update for the --actor mode
##### --show-poster-url: similar to blogger-url but this time for the actor photo-url
##### --delete-poster-url: similar to blogger-url but this time for the actor photo-url
##### --update-poster-url <url>: similar to blogger-url but this time for the actor photo-url
##### --list-poster-url [<n>]: similar to blogger-url but this time for the actor photo-url
##### --revert-poster-url <n>: similar to blogger-url but this time for the actor photo-url
#
# William Powell:
# <div class="separator" style="clear: both; text-align: center;">
# <a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEia7QyVwhtKpc7G1hBO8W9PGz8v8Mwp3OCEfnVhmFLlKGzKOxY-uKMzTBaPy8ERaz678x0PY_UajAN1gNTPqTTSNTM7bz75tAYAXKSmPkHzwOpHd5KUHfMPrvTXPtLdxhUugus74Vxm5aEIiYCEExhF1zHUm5R4AB3vOL1SfisKDjFeiGuJbbdtOkX3zD0K/s1256/MV5BMTk4MzcxMjExN15BMl5BanBnXkFtZTcwODA2NDQyOA@@._V1_.jpg" style="margin-left: 1em; margin-right: 1em;">
# <img border="0"
# data-original-height="1256"
# data-original-width="1000"
# height="640"
# src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEia7QyVwhtKpc7G1hBO8W9PGz8v8Mwp3OCEfnVhmFLlKGzKOxY-uKMzTBaPy8ERaz678x0PY_UajAN1gNTPqTTSNTM7bz75tAYAXKSmPkHzwOpHd5KUHfMPrvTXPtLdxhUugus74Vxm5aEIiYCEExhF1zHUm5R4AB3vOL1SfisKDjFeiGuJbbdtOkX3zD0K/w510-h640/MV5BMTk4MzcxMjExN15BMl5BanBnXkFtZTcwODA2NDQyOA@@._V1_.jpg"
# width="510" />
# </a>
# </div>
#
# Now that the script is working again, let's focus on --movie.
## --movie will require at least one of below.
#### --name <movie title> [--year <release-year]
#### --imdb <idmb-movie-id>
#### --tmdb <tmdb-movie-id>
## If there is no record in movie.db for the mvoie, download all the movieinfromation (exatcly as the same in --actor).
## Load all the cast for the mvoie and create entries in movie_credits.db. For each new cast member, create an entry in actor.db
## if --verbose is used, show messages for each new movie title created and each actor created
## if --force-update is used, even if the inforamtion for the movie and/or actor is present, gather it again from TMDB and/or IMDB and record any changes.
## after that the user has to chose the execution sub mode (as in --actor) [all the modes are mutually exclusive]
#### --blog -> IF the blogger-url field is empty generates an new entry in the blogger for this movie (similar to what happens with --actor)
#### --blog -> if the blogger-url field is not-empty, present the menu with the oprions for
######## a) /dev/clipboard.
######## b) output fo file <movie-id>.htlm
######## c) create a new blogger entry and replace the current movie.db blogger-url
######## d) update the existing blogger post (not activate at this stage)
######## If the user pass --force-clipboard, assume option (a)
######## if the user pass --force-file, assume option (b)
######## if the user pass --force-new, assume (c)
######## if the user pass --force-update, assume (d)
######## all those --force options are mutually exclusive
#
#### --show-blogger-url: show the current blogger url for the movie
#### --delete-blogger-url: clean/erase/dele/remove/nullify the current blogger url (preserve eveyrhting else for the movie record). record in log.
#### --update-blogger-url <url>: change the blogger url for a new url. record in log
#### --list-blogger-url [<n>]: list the previous <n> blogger url used (n defauult to 10)
#### --revert-blogger-url [<n>]: revert blogger url to previous state (n default to 1)
#
#### --show-poster-url: similar to blogger-url but this time for the movie photo-url
#### --delete-poster-url: similar to blogger-url but this time for the movie photo-url
#### --update-poster-url <url>: similar to blogger-url but this time for the movie photo-url
#### --list-poster-url [<n>]: similar to blogger-url but this time for the movie photo-url
#### --revert-poster-url <n>: similar to blogger-url but this time for the movie photo-url
#
#### --show-wacth: similar to blogger-url but this time for the the watch date
#### --delete-wacth: similar to blogger-url but this time for the watch date
#### --update-wacth <yyyy-mm-dd>: similar to blogger-url but this time for the watch date
#### --list-wacth [<n>]: similar to blogger-url but this time for the watch date
#### --revert-wacth <n>: similar to blogger-url but this time for the watch date
################################################################################
#grep -E '^\s*(function\s+)?[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)\s*\{' MovieUpdateDraft.sh | sed -E 's/^\s*(function\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\).*/\2/' | sort
# Load shared library
[[ ! -e ~/MY_LIBRARY.sh ]] && { echo "***ERROR: Cannot find library" ; exit 1; }
[[ ! -r ~/MY_LIBRARY.sh ]] && { echo "***ERROR: Library is not readable" ; exit 1; }
source ~/MY_LIBRARY.sh || { echo "***ERROR: Cannot source library" ; exit 1; }
[[ ! -e ~/MY_SECRETS.sh ]] && { echo "***ERROR: Cannot find ~/MY_SECRETS.sh" ; exit 1; }
[[ ! -r ~/MY_SECRETS.sh ]] && { echo "***ERROR: ~/MY_SECRETS.sh is not readable" ; exit 1; }
source ~/MY_SECRETS.sh || { echo "***ERROR: Cannot source ~/MY_SECRETS.sh" ; exit 1; }
set -o monitor
trap 'echo;Echo "💥 Caught interrupt signal, exiting."; exit 130' SIGINT SIGTERM
# Constants
readonly SCRIPT_VERSION="3.9"
readonly MOVIE_ROOT="/cygdrive/f/MediaLibrary/Movies/!By_Year"
# Variables
declare -A year_map=()
declare -a FILTER_YEARS=()
declare -a LABELS=()
declare -i VERBOSE=0
declare -i VERBOSE_DEPS=0
declare -i DRYRUN=0
declare -i DEBUG=0
declare -a POSITIONAL_ARGS=()
declare -i TMDB_KEY_VALID=0
declare -i CHECK_DEPS_DEFAULT=0
declare -i CHECK_DEPS_LOCAL=1
declare -i CHECK_DEPS_INSTALL=0
declare -i DRY_RUN=0
declare -i EXECUTION_MODE=0 # 0 = undefined, 1 = movie list, 2 = actor mode, etc.
declare -i EXECUTION_SUBMODE=0
#----- Movie List Variables
declare CUSTOM_DATE=""
declare -i USE_HTML=1
declare -i FORCE_PUBLISH=0
declare CUSTOM_TITLE=""
declare -i FORCE=0
declare -i FORCE_ACTORS=0
#----- ACTOR mode variable
declare ACTOR_QUERY=""
declare -i UPDATE_MODE=0
declare ACTOR_UPDATE_PATH=""
declare IMDB_ID=""
declare ACTOR_QUERY=""
declare ACTOR_IMDB_ID=""
declare NEW_BLOGGER_URL=
declare NEW_PHOTO_URL=
declare -gi FORCE_BIO=0
declare _ACTOR_SEARCH_RESULT_JSON=
# Metadata Cache
declare -A METADATA_CACHE=()
readonly METADATA_FILE_NAME="metadata.json"
readonly OMDB_CACHE_FILE="/tmp/.omdb_cache.json"
#******************************************************************************
#****************** Local Movie Database cache functions **********************
#******************************************************************************
#**************** This code should be move to MY_DB.sh later. It wil be sourced
#-----------------------------------------------------------------------------#
# |
# |
# C O N S T A N T S & V A R I A B L E S |
# |
# |
#-----------------------------------------------------------------------------#
readonly MY_MDB_CACHE="${HOME}/.MyMDBCache"
readonly ACTOR_DB_PATH="${MY_MDB_CACHE}/actor.db"
readonly MOVIE_DB_PATH="${MY_MDB_CACHE}/movie.db"
readonly MOVIE_CREDITS_DB_PATH="${MY_MDB_CACHE}/movie_credits.db"
readonly LINKS_DB_PATH="${MY_MDB_CACHE}/links.db"
readonly MANY_RECORD_MARK="***MANY***"
declare ACTOR_RECORD_LOADED=
declare ACTOR_TMDB_ID=
declare ACTOR_IMDB_ID=
declare ACTOR_NAME=
declare ACTOR_DOB=
declare ACTOR_DOD=
declare ACTOR_PHOTO_URL=
declare ACTOR_GENDER=
declare ACTOR_DEPTO=
declare ACTOR_BLOG_URL=
declare ACTOR_BIRTH_YEAR=
declare MOVIE_RECORD_LOADED=
declare MOVIE_TMDB_ID
declare MOVIE_IMDB_ID
declare MOVIE_COUNTRY
declare MOVIE_TITLE_EN
declare MOVIE_TITLE_ORIG
declare MOVIE_RELEASE_DATE
declare MOVIE_EXISTING_WATCHED_DATE
declare MOVIE_BLOGGER_URL
declare MOVIE_PHOTO_URL
#-----------------------------------------------------------------------------#
# |
# |
# A C T O R |
# |
# |
#-----------------------------------------------------------------------------#
#==============================================================================
# DBMovie_InitActorDb: Initialize actor.db if missing or empty
#------------------------------------------------------------------------------
# Description:
# Ensures that the actor metadata cache file (`actor.db`) exists and contains
# the appropriate header. If the file does not exist or is empty, it will be
# created with the following pipe-separated column headers:
#
# TMDB_ID|IMDB_ID|NAME|DOB|DOD|GENDER|DEPARTMENT|PHOTO_URL|BLOGGER_URL
#
# Globals Used:
# ACTOR_DB_PATH - Full path to the actor.db file
# DEBUG - If set, logs the creation of the file
#
# Outputs:
# - Creates actor.db with header if the file is missing or empty
# - Logs a debug message if DEBUG is enabled
#
# Return:
# 0 - Success
# 1 - Failure (unable to create file)
#==============================================================================
DBMovie_InitActorDb() {
local -r file="${ACTOR_DB_PATH}"
if [[ ! -s "${file}" ]]; then
mkdir -p "$(dirname "${file}")" 2>/dev/null
if [[ ! -s "${file}" ]]; then
echo "TMDB_ID|IMDB_ID|NAME|DOB|DOD|GENDER|DEPARTMENT|PHOTO_URL|BLOGGER_URL" > "${file}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to create ${file}."
return 1
}
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Created ${file} with new header."
fi
fi
return 0
}
#==============================================================================
# ResolveActorTmdbIdFromImdb
#
# Given an IMDb ID, find and return the corresponding TMDb ID from actor.db.
#
# Arguments:
# ${1} - IMDb ID (e.g., nm0000493)
#
# Returns:
# Echoes the TMDb ID to stdout
# 0 - if found
# 1 - if not found
#==============================================================================
ResolveActorTmdbIdFromImdb() {
local -r imdb_id="${1}"
local -r file="${ACTOR_DB_PATH}"
[[ -z "${imdb_id}" ]] && return 1
DBMovie_InitActorDb || return 1
awk -F'|' -v id="${imdb_id}" '$2 == id { print $1; exit }' "${file}" | grep -q . && return 0 || return 1
}
#==============================================================================
# ResolveActorImdbIdFromTmdb
#
# Given a TMDb ID, find and return the corresponding IMDb ID from actor.db.
#
# Arguments:
# ${1} - TMDb ID (e.g., 32428)
#
# Returns:
# Echoes the IMDb ID to stdout
# 0 - if found
# 1 - if not found
#==============================================================================
ResolveActorImdbIdFromTmdb() {
local -r tmdb_id="${1}"
local -r file="${ACTOR_DB_PATH}"
[[ -z "${tmdb_id}" ]] && return 1
DBMovie_InitActorDb || return 1
awk -F'|' -v id="${tmdb_id}" '$1 == id { print $2; exit }' "${file}" | grep -q . && return 0 || return 1
}
#==============================================================================
# ResolveActorIds
#
# Ensures both IMDb and TMDb IDs are known for a given actor, first checking
# local cache (actor.db) before querying TMDb API. Updates local cache with
# full available metadata if missing.
#
# Arguments:
# ${1} - TMDb ID (e.g., 32428) or empty
# ${2} - IMDb ID (e.g., nm0000493) or empty
#
# Globals modified:
# ACTOR_TMDB_ID
# ACTOR_IMDB_ID
#
# Outputs:
# - Echoes the resolved TMDb ID to stdout
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
ResolveActorIds() {
local tmdb_id_arg=""
local imdb_id_arg=""
local record=""
local -r FUNC="${FUNCNAME[0]}"
# Handle single-parameter usage (auto-detect type)
if [[ -n "${1}" && -z "${2}" ]]; then
if [[ "${1}" =~ ^nm[0-9]+$ ]]; then
imdb_id_arg="${1}"
elif [[ "${1}" =~ ^[0-9]+$ ]]; then
tmdb_id_arg="${1}"
else
EchoE "${FUNC}:${LINENO}: Invalid actor ID format: '${1}'"
return 1
fi
else
tmdb_id_arg="${1:-}"
imdb_id_arg="${2:-}"
fi
readonly tmdb_id_arg
readonly imdb_id_arg
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: TMDb ID=${tmdb_id_arg}, IMDb ID=${imdb_id_arg}"
#-----------------------------
# Step 1: Try cache via TMDb ID
#-----------------------------
if [[ -n "${tmdb_id_arg}" ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Attempting cache lookup by TMDb ID..."
if record=$(DBMovie_LoadActorRecord "${tmdb_id_arg}"); then
IFS='|' read -r actor_tmdb actor_imdb _ <<< "${record}"
if [[ -n "${actor_imdb}" ]]; then
export ACTOR_TMDB_ID="${actor_tmdb}"
export ACTOR_IMDB_ID="${actor_imdb}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found in cache: TMDb=${ACTOR_TMDB_ID}, IMDb=${ACTOR_IMDB_ID}"
return 0
else
EchoD "${FUNC}:${LINENO}: IMDb ID missing in local cache, need online resolution."
fi
fi
fi
#-----------------------------
# Step 2: Try cache via IMDb ID
#-----------------------------
if [[ -n "${imdb_id_arg}" ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Attempting cache lookup by IMDb ID..."
if record=$(DBMovie_LoadActorRecord "${imdb_id_arg}"); then
IFS='|' read -r actor_tmdb actor_imdb _ <<< "${record}"
if [[ -n "${actor_tmdb}" ]]; then
export ACTOR_TMDB_ID="${actor_tmdb}"
export ACTOR_IMDB_ID="${actor_imdb}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found in cache: TMDb=${ACTOR_TMDB_ID}, IMDb=${ACTOR_IMDB_ID}"
return 0
fi
fi
fi
#-----------------------------
# Step 3: Online resolution
#-----------------------------
local response=""
local tmdb_id=""
local imdb_id=""
local name=""
local dob=""
local dod=""
local photo_url=""
local gender=""
local department=""
if [[ -n "${imdb_id_arg}" ]]; then
EchoW "${FUNC}:${LINENO}: IMDb ID '${imdb_id_arg}' not found locally, querying TMDb..."
response=$(curl --silent --fail \
"https://api.themoviedb.org/3/find/${imdb_id_arg}?api_key=${TMDB_API_KEY}&external_source=imdb_id") || {
EchoE "${FUNC}:${LINENO}: Failed to resolve via IMDb ID ${imdb_id_arg}."
return 1
}
tmdb_id=$(jq -r '.person_results[0].id // empty' <<< "${response}")
name=$(jq -r '.person_results[0].name // empty' <<< "${response}")
dob=$(jq -r '.person_results[0].birthday // empty' <<< "${response}")
dod=$(jq -r '.person_results[0].deathday // empty' <<< "${response}")
photo_url=$(jq -r '.person_results[0].profile_path // empty' <<< "${response}")
gender=$(jq -r '.person_results[0].gender // empty' <<< "${response}")
department=$(jq -r '.person_results[0].known_for_department // empty' <<< "${response}")
[[ -n "${photo_url}" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"
if [[ -z "${tmdb_id}" ]]; then
EchoE "${FUNC}:${LINENO}: No TMDb ID found for IMDb ID ${imdb_id_arg}."
return 1
fi
DBMovie_SaveActorRecord "${tmdb_id}" "${imdb_id_arg}" "${name}" "${dob}" "${dod}" "${gender}" "${department}" "${photo_url}" ""
export ACTOR_TMDB_ID="${tmdb_id}"
export ACTOR_IMDB_ID="${imdb_id_arg}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Resolved and cached: TMDb=${tmdb_id}, IMDb=${imdb_id_arg}"
return 0
fi
if [[ -n "${tmdb_id_arg}" ]]; then
EchoW "${FUNC}:${LINENO}: TMDb ID '${tmdb_id_arg}' not found locally, querying TMDb..."
response=$(curl --silent --fail \
"https://api.themoviedb.org/3/person/${tmdb_id_arg}?api_key=${TMDB_API_KEY}") || {
EchoE "${FUNC}:${LINENO}: Failed to resolve via TMDb ID ${tmdb_id_arg}."
return 1
}
imdb_id=$(jq -r '.imdb_id // empty' <<< "${response}")
name=$(jq -r '.name // empty' <<< "${response}")
dob=$(jq -r '.birthday // empty' <<< "${response}")
dod=$(jq -r '.deathday // empty' <<< "${response}")
photo_url=$(jq -r '.profile_path // empty' <<< "${response}")
gender=$(jq -r '.gender // empty' <<< "${response}")
department=$(jq -r '.known_for_department // empty' <<< "${response}")
[[ -n "${photo_url}" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"
if [[ -z "${imdb_id}" ]]; then
EchoE "${FUNC}:${LINENO}: No IMDb ID found for TMDb ID ${tmdb_id_arg}."
return 1
fi
DBMovie_SaveActorRecord "${tmdb_id_arg}" "${imdb_id}" "${name}" "${dob}" "${dod}" "${gender}" "${department}" "${photo_url}" ""
export ACTOR_TMDB_ID="${tmdb_id_arg}"
export ACTOR_IMDB_ID="${imdb_id}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Resolved and cached: TMDb=${tmdb_id_arg}, IMDb=${imdb_id}"
return 0
fi
EchoE "${FUNC}:${LINENO}: No valid TMDb or IMDb ID provided."
return 1
}
#==============================================================================
# DBMovie_SaveActorRecord
#
# Save or update a record in actor.db using TMDb ID as the primary key.
# If TMDb ID is missing, falls back to IMDb ID. Sanitizes all fields.
#
# Globals used:
# - ACTOR_DB_PATH
#
# Arguments:
# ${1} - TMDb ID (e.g., 123456)
# ${2} - IMDb ID (e.g., nm0000123)
# ${3} - Name
# ${4} - Date of birth (YYYY-MM-DD)
# ${5} - Date of death (YYYY-MM-DD)
# ${6} - Gender (Male, Female, Unknown)
# ${7} - Department (e.g., Acting, Directing)
# ${8} - Photo URL
# ${9} - Blogger post URL
#
# Outputs:
# - Updates actor.db safely
# - Logs debug info via EchoD
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_SaveActorRecord() {
local -r tmdb_id="${1}"
local -r imdb_id="${2}"
local name="${3}"
local -r dob="${4}"
local -r dod="${5}"
local gender="${6}"
local department="${7}"
local photo_url="${8}"
local blog_url="${9}"
local -r file="${ACTOR_DB_PATH}"
DBMovie_InitActorDb || return 1
local -r safe_tmdb_id="$(SanitizeField "${tmdb_id}")"
local -r safe_imdb_id="$(SanitizeField "${imdb_id}")"
name="$(SanitizeField "${name}")"
local -r safe_dob="$(SanitizeField "${dob}")"
local -r safe_dod="$(SanitizeField "${dod}")"
gender="$(SanitizeField "${gender}")"
department="$(SanitizeField "${department}")"
local -r safe_photo_url="$(SanitizeField "${photo_url}")"
local -r safe_blog_url="$(SanitizeField "${blog_url}")"
# Validate minimal requirements
if [[ -z "${safe_tmdb_id}" && -z "${safe_imdb_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot save actor record — both TMDb and IMDb IDs are missing."
return 1
fi
if [[ -n "${safe_tmdb_id}" && ! "${safe_tmdb_id}" =~ ^[0-9]+$ ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Invalid TMDb ID format '${safe_tmdb_id}'. Must be numeric."
return 1
fi
if [[ -n "${safe_imdb_id}" && ! "${safe_imdb_id}" =~ ^nm[0-9]+$ ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Invalid IMDb ID format '${safe_imdb_id}'. Must start with 'nm'."
return 1
fi
# Ensure database file exists
DBMovie_InitActorDb || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to initialize actor.db"
return 1
}
# Remove any existing record by TMDb ID or IMDb ID
grep -v -E "^(${safe_tmdb_id}\||${safe_imdb_id}\|)" "${file}" > "${file}.tmp" 2>/dev/null || true
# Append the sanitized new record
echo "${safe_tmdb_id}|${safe_imdb_id}|${name}|${safe_dob}|${safe_dod}|${gender}|${department}|${safe_photo_url}|${safe_blog_url}" >> "${file}.tmp"
EchoD "${FUNCNAME[0]}:${LINENO}: record='${safe_tmdb_id}|${safe_imdb_id}|${name}|${safe_dob}|${safe_dod}|${gender}|${department}|${safe_photo_url}|${safe_blog_url}'"
# Replace the database
mv "${file}.tmp" "${file}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to update actor.db"
return 1
}
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Saved actor: TMDb=${safe_tmdb_id}, IMDb=${safe_imdb_id}, Name='${name}'"
return 0
}
#==============================================================================
# DBMovie_LoadActorRecordOnline
#
# Fetch an actor's metadata online using TMDb API and optionally IMDb,
# and create a new local record in actor.db.
#
# Globals used:
# - TMDB_API_KEY
# - ACTOR_DB_PATH
#
# Globals modified:
# - actor.db (new record inserted)
#
# Arguments:
# ${1} - Actor ID (TMDb ID or IMDb ID)
#
# Return:
# 0 - Success (record created)
# 1 - Failure (record not fetched)
#==============================================================================
DBMovie_LoadActorRecordOnline() {
EchoE "${FUNCNAME[0]}:${LINENO}: 1=${1}"
local -r id="${1}"
local tmdb_id=""
local imdb_id=""
DBMovie_InitActorDb || return 1
# Resolve whether ID is IMDb or TMDb
if [[ "${id}" =~ ^nm[0-9]+$ ]]; then
# IMDb ID — resolve to TMDb
tmdb_id="$(ResolveActorTmdbIdFromImdb "${id}")" || return 1
imdb_id="${id}"
else
# Assume TMDb ID
tmdb_id="${id}"
imdb_id="$(ResolveActorImdbIdFromTmdb "${id}")" || return 1
fi
EchoE "${FUNCNAME[0]}:${LINENO}: tmdb_id=${tmdb_id} imdb_id=${imdb_id}"
# Fetch full actor details
local json
json="$(curl --silent --fail "https://api.themoviedb.org/3/person/${tmdb_id}?api_key=${TMDB_API_KEY}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch actor metadata from TMDb."
return 1
}
# Parse fields
local name dob dod gender_code department photo_url gender_text=""
name="$(jq -r '.name // empty' <<< "${json}")"
dob="$(jq -r '.birthday // empty' <<< "${json}")"
dod="$(jq -r '.deathday // empty' <<< "${json}")"
gender_code="$(jq -r '.gender // empty' <<< "${json}")"
department="$(jq -r '.known_for_department // empty' <<< "${json}")"
photo_url="$(jq -r '.profile_path // empty' <<< "${json}")"
[[ -n "${photo_url}" && "${photo_url}" != "null" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"
if [[ -z "${name}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Actor metadata incomplete, cannot create record."
return 1
fi
# Save to cache
DBMovie_SaveActorRecord "${tmdb_id}" "${imdb_id}" "${name}" "${dob}" "${dod}" "${gender_code}" "${department}" "${photo_url}" "" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to save actor record."
return 1
}
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Saved actor '${name}' (TMDb ID=${tmdb_id}) to cache."
return 0
}
#==============================================================================
# DBMovie_LoadActorRecord
#
# Load an actor's full record from actor.db. If not found, fetch online
# and update the cache automatically.
#
# Globals Used:
# ACTOR_DB_PATH
# ACTOR_RECORD_LOADED
#
# Globals Modified:
# ACTOR_RECORD_LOADED
#
# Arguments:
# ${1} - Actor ID (TMDb ID or IMDb ID)
#
# Return:
# 0 - Success (record loaded)
# 1 - Failure
#==============================================================================
DBMovie_LoadActorRecord() {
local -r id="${1}"
local -r file="${ACTOR_DB_PATH}"
local -r FUNC="${FUNCNAME[0]}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: 1=${1}"
if [[ -z "${id}" ]]; then
EchoE "${FUNC}:${LINENO}: No ID provided."
return 1
fi
DBMovie_InitActorDb || return 1
local record=""
local -i attempt=0
while (( attempt < 2 )); do
if [[ "${id}" =~ ^nm[0-9]+$ ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: IMDB id=${id}"
# IMDb ID lookup (field 2)
record="$(awk -F'|' -v key="${id}" 'NR>1 && $2==key {print; exit}' "${file}")"
else
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: TMDB id=${id}"
# TMDb ID lookup (field 1)
record="$(awk -F'|' -v key="${id}" 'NR>1 && $1==key {print; exit}' "${file}")"
fi
if [[ -n "${record}" ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: record=${record}"
export ACTOR_RECORD_LOADED="${record}"
echo "${record}"
return 0
fi
if (( attempt == 0 )); then
EchoW "${FUNC}:${LINENO}: Actor ID '${id}' not found locally. Attempting online retrieval..."
DBMovie_LoadActorRecordOnline "${id}" >/dev/null || {
EchoE "${FUNC}:${LINENO}: Failed to retrieve actor ID '${id}' online."
return 1
}
fi
(( attempt++ ))
done
EchoE "${FUNC}:${LINENO}: Actor ID '${id}' still not found after online retrieval."
return 1
}
#==============================================================================
# DBMovie_LoadActorRecordParse
#
# Fetch actor metadata from TMDb and IMDb and store it in the local cache.
#
# Arguments:
# $1 - TMDb ID (e.g., "12345") or empty
# $2 - IMDb ID (e.g., "nm0000001") or empty
#
# Globals Used:
# - TMDB_API_KEY
# - MY_MDB_CACHE
#
# Globals Modified:
# - ACTOR_TMDB_ID
# - ACTOR_IMDB_ID
# - ACTOR_NAME
# - ACTOR_DOB
# - ACTOR_DOD
# - ACTOR_GENDER
# - ACTOR_DEPTO
# - ACTOR_PHOTO_URL
# - ACTOR_BLOG_URL
# - ACTOR_BIRTH_YEAR
#
# Returns:
# 0 on success, 1 on failure
#==============================================================================
DBMovie_LoadActorRecordParse() {
local -r tmdb_id_arg="${1:-}"
local -r imdb_id_arg="${2:-}"
local -r FUNC="${FUNCNAME[0]}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: tmdb_id=${1} imdb_id=${2}"
DBMovie_InitActorDb || return 1
ResolveActorIds "${tmdb_id_arg}" "${imdb_id_arg}" || return 1
local -r tmdb_id="${ACTOR_TMDB_ID}"
local -r imdb_id="${ACTOR_IMDB_ID}"
local record=""
if record="$(DBMovie_LoadActorRecord "${tmdb_id}")"; then
local rec_tmdb_id rec_imdb_id rec_name rec_dob rec_dod rec_gender rec_depto rec_photo rec_blog
IFS='|' read -r rec_tmdb_id rec_imdb_id rec_name rec_dob rec_dod rec_gender rec_depto rec_photo rec_blog <<< "${record}"
export ACTOR_TMDB_ID="${rec_tmdb_id}"
export ACTOR_IMDB_ID="${rec_imdb_id}"
export ACTOR_NAME="${rec_name}"
export ACTOR_DOB="${rec_dob}"
export ACTOR_DOD="${rec_dod}"
export ACTOR_GENDER="${rec_gender}"
export ACTOR_DEPTO="${rec_depto}"
export ACTOR_PHOTO_URL="${rec_photo}"
export ACTOR_BLOG_URL="${rec_blog}"
export ACTOR_BIRTH_YEAR="${rec_dob:0:4}"
Echo "📇 Loaded actor details from cache: ${ACTOR_NAME} (${ACTOR_DOB:-N/A}..${ACTOR_DOD:-N/A}) [TMDb: ${ACTOR_TMDB_ID}, IMDb: ${ACTOR_IMDB_ID}]"
return 0
else
EchoE "${FUNC}:${LINENO}: Cannot read actor record from cache [TMDb=${tmdb_id}, IMDb=${imdb_id}]"
return 1
fi
}
#==============================================================================
# DBMovie_UpdateActorBloggerUrl
#
# Update the Blogger URL field for an existing actor record.
#
# Globals used:
# - ACTOR_DB_PATH
#
# Globals modified:
# - actor.db (one record updated)
#
# Arguments:
# ${1} - TMDb ID or IMDb ID of the actor
# ${2} - New Blogger URL (unsanitized)
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_UpdateActorBloggerUrl() {
local -r id="${1}"
local -r new_url_unsanitized="${2}"
local -r FUNC="${FUNCNAME[0]}"
if [[ -z "${id}" || -z "${new_url_unsanitized}" ]]; then
EchoE "${FUNC}:${LINENO}: Missing ID or new Blogger URL."
return 1
fi
DBMovie_InitActorDb || return 1
DBMovie_LoadActorRecord "${id}" >/dev/null || {
EchoE "${FUNC}:${LINENO}: Cannot update Blogger URL — actor ID '${id}' not found."
return 1
}
local -a fields
IFS='|' read -r -a fields <<< "${ACTOR_RECORD_LOADED}"
# Ensure 9 fields (TMDB|IMDB|NAME|DOB|DOD|GENDER|DEPARTMENT|PHOTO_URL|BLOGGER_URL)
while (( ${#fields[@]} < 9 )); do
fields+=("")
done
# Save updated record
DBMovie_SaveActorRecord \
"${fields[0]}" \
"${fields[1]}" \
"${fields[2]}" \
"${fields[3]}" \
"${fields[4]}" \
"${fields[5]}" \
"${fields[6]}" \
"${fields[7]}" \
"$(SanitizeField "${new_url_unsanitized}")"
return 0
}
#==============================================================================
# DBMovie_GetActorBloggerUrl
#
# Return the Blogger URL for an actor given TMDb ID or IMDb ID.
#
# Globals Used:
# - ACTOR_DB_PATH
# - ACTOR_RECORD_LOADED
#
# Arguments:
# ${1} - TMDb ID or IMDb ID
#
# Outputs:
# - Echoes the Blogger URL (9th field) if found
# - Returns 1 if not found
#
# Return:
# 0 - Success (Blogger URL printed)
# 1 - Failure (actor not found or missing URL)
#==============================================================================
DBMovie_GetActorBloggerUrl() {
local -r id="${1}"
local -r file="${ACTOR_DB_PATH}"
if [[ -z "${id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: No ID provided."
return 1
fi
DBMovie_InitActorDb || return 1
DBMovie_LoadActorRecord "${id}" >/dev/null || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot load actor record for ID '${id}'."
return 1
}
local -a fields
IFS='|' read -r -a fields <<< "${ACTOR_RECORD_LOADED}"
# Ensure at least 9 fields
while (( ${#fields[@]} < 9 )); do
fields+=("")
done
local -r blogger_url="${fields[8]}"
if [[ -n "${blogger_url}" ]]; then
echo "${blogger_url}"
return 0
else
return 1
fi
}
#==============================================================================
# DBMovie_UpdateActorPhotoUrl
#
# Update the photo URL field for an existing actor record.
#
# Globals Used:
# - ACTOR_DB_PATH
# - ACTOR_RECORD_LOADED
#
# Globals Modified:
# - actor.db (one record updated)
#
# Arguments:
# ${1} - TMDb ID or IMDb ID
# ${2} - New photo URL (unsanitized)
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_UpdateActorPhotoUrl() {
local -r id="${1}"
local -r new_url="${2}"
local -r FUNC="${FUNCNAME[0]}"
if [[ -z "${id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: No ID provided."
return 1
fi
DBMovie_InitActorDb || return 1
DBMovie_LoadActorRecord "${id}" >/dev/null || {
EchoE "${FUNC}:${LINENO}: Cannot update photo URL: actor not found."
return 1
}
local -a fields
IFS='|' read -r -a fields <<< "${ACTOR_RECORD_LOADED}"
# Ensure fields array has at least 9 elements (now includes Gender, Department)
while (( ${#fields[@]} < 9 )); do
fields+=("")
done
DBMovie_SaveActorRecord \
"${fields[0]}" \
"${fields[1]}" \
"${fields[2]}" \
"${fields[3]}" \
"${fields[4]}" \
"${fields[5]}" \
"${fields[6]}" \
"$(SanitizeField "${new_url}")" \
"${fields[8]}"
return 0
}
#==============================================================================
# DBMovie_GetActorPhotoUrl: Retrieve actor photo URL from actor.db
#------------------------------------------------------------------------------
# Description:
# Given either a TMDb ID or IMDb ID, this function retrieves the actor's
# photo URL (field 8) by loading the record via DBMovie_LoadActorRecord.
#
# Globals:
# ACTOR_DB_PATH - Path to actor.db (used internally)
# ACTOR_RECORD_LOADED - Set by DBMovie_LoadActorRecord
#
# Arguments:
# ${1} - TMDb ID or IMDb ID
#
# Outputs:
# - Echoes the photo URL (field 8) to stdout
# - Returns 0 if found, 1 on failure
#==============================================================================
DBMovie_GetActorPhotoUrl() {
local -r input_id="${1:-}"
if [[ -z "${input_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: No ID provided."
return 1
fi
DBMovie_InitActorDb || return 1
if ! DBMovie_LoadActorRecord "${input_id}" 2>/dev/null; then
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to load actor record for ID '${input_id}'."
return 1
fi
local -a fields
IFS='|' read -r -a fields <<< "${ACTOR_RECORD_LOADED}"
# Ensure at least 9 fields (TMDB, IMDB, NAME, DOB, DOD, GENDER, DEPARTMENT, PHOTO_URL, BLOGGER_URL)
while (( ${#fields[@]} < 9 )); do
fields+=("")
done
local -r photo_url="${fields[7]}" # Field 8 is PHOTO_URL
if [[ -n "${photo_url}" ]]; then
echo "${photo_url}"
return 0
fi
return 1
}
#==============================================================================
# DBMovie_IsActorCached
#
# Check if an actor record exists in actor.db by TMDb ID or IMDb ID.
# Tries TMDb ID first (primary key), then IMDb ID (secondary key).
#
# Globals used:
# - ACTOR_DB_PATH
#
# Arguments:
# ${1} - Actor ID (TMDb ID preferred, or IMDb ID as fallback)
#
# Return:
# 0 - Found
# 1 - Not found
#==============================================================================
DBMovie_IsActorCached() {
local -r actor_id="${1:-}"
local -r file="${ACTOR_DB_PATH}"
if [[ -z "${actor_id}" ]]; then
return 1
fi
DBMovie_InitActorDb || return 1
# First try exact TMDb ID match (2nd column)
if awk -F'|' -v id="${actor_id}" 'NR>1 && $2==id {exit 0} END {exit 1}' "${file}"; then
return 0
fi
# Then try exact IMDb ID match (1st column)
if awk -F'|' -v id="${actor_id}" 'NR>1 && $1==id {exit 0} END {exit 1}' "${file}"; then
return 0
fi
return 1
}
#==============================================================================
# GetActorTmdbIdFromName
#
# Resolve actor name to TMDb ID, using local cache first. Saves multiple matches
# found online immediately to the local actor.db cache to avoid future API hits.
# If multiple matches exist, asks user to select one.
#
# Arguments:
# ${1} - Actor name (e.g., "William Powell")
#
# Globals Used:
# - ACTOR_DB_PATH
# - TMDB_API_KEY
#
# Globals Modified:
# - ACTOR_TMDB_ID_RESULT
# - _ACTOR_SEARCH_RESULT_JSON
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
GetActorTmdbIdFromName() {
local actor_name="$(SanitizeField "${1:-}")"
local -a matches
local line name match_tmdb_id local query_result count
local -r FUNC="${FUNCNAME[0]}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: $*"
if [[ -z "${actor_name}" ]]; then
EchoE "${FUNC}:${LINENO}: Actor name is empty."
return 1
fi
DBMovie_InitActorDb || return 1
local reload_online=false
while true; do
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: reload_online=${reload_online}"
matches=()
if [[ ${reload_online} == false ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
while IFS='|' read -r tmdb_id imdb_id name _; do
if [[ "${name,,}" == "${actor_name,,}" && "${tmdb_id}" =~ ^[0-9]+$ ]]; then
matches+=("${tmdb_id}")
fi
done < "${ACTOR_DB_PATH}"
fi
if (( ${#matches[@]} == 1 )); then
ACTOR_TMDB_ID_RESULT="${matches[0]}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found single match in cache: TMDb ID ${ACTOR_TMDB_ID_RESULT}"
elif (( ${#matches[@]} > 1 )); then
ACTOR_TMDB_ID_RESULT="${MANY_RECORD_MARK}"
EchoW "${FUNC}:${LINENO}: Multiple matches found locally for '${actor_name}'."
else
EchoW "${FUNC}:${LINENO}: No local match for '${actor_name}', querying TMDb..."
query_result="$(curl --silent --fail \
"https://api.themoviedb.org/3/search/person?api_key=${TMDB_API_KEY}&query=$(urlencode "${actor_name}")")" || {
EchoE "${FUNC}:${LINENO}: Failed to contact TMDb."
return 1
}
count=$(jq '.results | length' <<< "${query_result}")
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: count=${count}"
if (( count == 0 )); then
ACTOR_TMDB_ID_RESULT=""
EchoW "${FUNC}:${LINENO}: No actor found on TMDb."
return 1
elif (( count >= 1 )); then
_ACTOR_SEARCH_RESULT_JSON="${query_result}"
local i tmdb_id imdb_id name dob dod photo_url gender department
matches=()
for (( i = 0; i < count; i++ )); do
tmdb_id=$(jq -r ".results[${i}].id // empty" <<< "${query_result}")
name=$(jq -r ".results[${i}].name // empty" <<< "${query_result}")
# Fetch detailed info
full_info=$(curl --silent --fail "https://api.themoviedb.org/3/person/${tmdb_id}?api_key=${TMDB_API_KEY}") || {
EchoW "${FUNC}:${LINENO}: Failed to fetch detailed info for TMDb ID ${tmdb_id}."
continue
}
dob=$(jq -r '.birthday // empty' <<< "${full_info}")
dod=$(jq -r '.deathday // empty' <<< "${full_info}")
gender=$(jq -r '.gender // empty' <<< "${full_info}")
department=$(jq -r '.known_for_department // empty' <<< "${full_info}")
photo_url=$(jq -r '.profile_path // empty' <<< "${full_info}")
imdb_id=$(jq -r '.imdb_id // empty' <<< "${full_info}")
matches+=("${tmdb_id}")
# Fix photo URL
[[ -n "${photo_url}" && "${photo_url}" != "null" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: '${tmdb_id}' '${imdb_id}' '${name}'"
if [[ -n "${tmdb_id}" && -n "${name}" ]]; then
DBMovie_SaveActorRecord \
"${tmdb_id}" \
"${imdb_id}" \
"${name}" \
"${dob}" \
"${dod}" \
"${gender}" \
"${department}" \
"${photo_url}" \
"" || {
EchoW "${FUNC}:${LINENO}: Failed to save actor '${name}' (TMDb ID=${tmdb_id}) to cache."
}
fi
done
if (( count == 1 )); then
ACTOR_TMDB_ID_RESULT="$(jq -r '.results[0].id' <<< "${query_result}")"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Single match TMDb ID: ${ACTOR_TMDB_ID_RESULT}"
else
ACTOR_TMDB_ID_RESULT="${MANY_RECORD_MARK}"
EchoW "${FUNC}:${LINENO}: Multiple matches found online for '${actor_name}'."
fi
fi
fi
if [[ "${ACTOR_TMDB_ID_RESULT}" != "${MANY_RECORD_MARK}" ]]; then
return 0
fi
local -a ids names departments
local -i idx=0
# Rebuild ids[], dobs[], dods[], genders[], names[], departments[] from local cache
while IFS='|' read -r tmdb_id imdb_id name dob dod gender department photo_url blog_url; do
for match in "${matches[@]}"; do
if [[ "${match}" == "${tmdb_id}" ]]; then
ids[idx]="${tmdb_id}"
dobs[idx]="${dob}"
dods[idx]="${dod}"
genders[idx]="${gender}"
names[idx]="${name}"
departments[idx]="${department}"
((idx++))
break
fi
done
done < "${ACTOR_DB_PATH}"
if (( idx == 0 )); then
EchoE "${FUNC}:${LINENO}: No valid actor entries to select."
return 1
fi
Echo
Echo "Idx TMDB_ID DoB DoD Gender Department Name"
Echo "--- ------- ---------- ---------- ---------- ----------------- ----------------------------"
for ((i = 0; i < idx; i++)); do
local gender_label=
case "${genders[i]:-2}" in
0) gender_label="Male" ;;
1) gender_label="Female" ;;
*) gender_label="Unknown" ;;
esac
Echo "$(printf "%-4s %-8s %-10s %-10s %-10s %-18s %s\n" \
"${i}" \
"${ids[i]}" \
"${dobs[i]:-N/A}" \
"${dods[i]:-N/A}" \
"${gender_label}" \
"${departments[i]:-N/A}" \
"${names[i]:-Unknown}")"
done
Echo
Echo "To skip this selection, next time use --tmdb <code> or --imdb <code>"
Echo "To force reload of names from online, use --force-name-online"
local choice=""
while true; do
EchoN "Select the correct TMDb ID [0-$((idx-1))], 'r' to force online reload, or 'q' to quit: "
read -r choice
choice="${choice,,}"
if [[ "${choice}" =~ ^[0-9]+$ ]] && (( choice >= 0 && choice < idx )); then
ACTOR_TMDB_ID_RESULT="${ids[choice]}"
Echo "✅ Using TMDb ID ${ACTOR_TMDB_ID_RESULT}"
return 0
elif [[ "${choice}" == r ]]; then
reload_online=true
break
elif [[ "${choice}" == q ]]; then
Echo "❌ Cancelled by user."
return 1
fi
EchoW "Invalid input. Please try again."
done
done
}
#-----------------------------------------------------------------------------#
# |
# |
# M O V I E |
# |
# |
#-----------------------------------------------------------------------------#
#==============================================================================
# ResolveMovieTmdbIdFromImdb
#
# Resolves a TMDb movie ID from a given IMDb ID using the TMDb API.
#
# Arguments:
# $1 - IMDb movie ID (e.g., tt0033467)
#
# Globals Used:
# TMDB_API_KEY
#
# Returns:
# 0 on success (prints TMDb ID to stdout)
# 1 on failure
#==============================================================================
ResolveMovieTmdbIdFromImdb() {
local -r imdb_id="${1:-}"
local -r FUNC="${FUNCNAME[0]}"
if [[ -z "${imdb_id}" ]]; then
EchoE "${FUNC}:${LINENO}: IMDb ID is required."
return 1
fi
if [[ ! "${imdb_id}" =~ ^tt[0-9]+$ ]]; then
EchoE "${FUNC}:${LINENO}: Invalid IMDb ID format '${imdb_id}'"
return 1
fi
local -r url="https://api.themoviedb.org/3/find/${imdb_id}?api_key=${TMDB_API_KEY}&external_source=imdb_id"
local response=""
response="$(curl --silent --fail "${url}")" || {
EchoE "${FUNC}:${LINENO}: Failed to fetch from TMDb for IMDb ID ${imdb_id}"
return 1
}
local -r tmdb_id="$(jq -r '.movie_results[0].id // empty' <<< "${response}")"
if [[ -z "${tmdb_id}" ]]; then
EchoE "${FUNC}:${LINENO}: No TMDb match found for IMDb ID ${imdb_id}"
return 1
fi
EchoD "${FUNC}:${LINENO}: IMDb ${imdb_id} → TMDb ${tmdb_id}"
echo "${tmdb_id}"
return 0
}
#==============================================================================
# DBMovie_InitMovieDb
#
# Initialize movie.db if missing or empty. Ensures correct field structure.
#
# Globals used:
# - MOVIE_DB_PATH
# - DEBUG
#
# Outputs:
# - Creates movie.db with a header line if the file is missing or empty.
# - Logs a debug message on creation if DEBUG is set.
#
# Return:
# 0 - Success
# 1 - Failure (unable to create file)
#==============================================================================
DBMovie_InitMovieDb() {
local -r file="${MOVIE_DB_PATH}"
if [[ ! -s "${file}" ]]; then
mkdir -p "$(dirname "${file}")" 2>/dev/null
if [[ ! -s "${file}" ]]; then
echo "TMDB_ID|IMDB_ID|COUNTRY|TITLE_EN|TITLE_ORIG|RELEASE_DATE|WATCHED_DATE|BLOGGER_URL|PHOTO_URL" > "${file}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to create ${file}."
return 1
}
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Created ${file} with correct header."
fi
fi
return 0
}
#==============================================================================
# DBMovie_SaveMovieRecord
#
# Save or update a movie record in movie.db.
# Overwrites existing TMDb or IMDb ID match, or appends new record.
#
# Globals used:
# - MOVIE_DB_PATH
#
# Arguments:
# ${1} - TMDb ID
# ${2} - IMDb ID
# ${3} - Country (ISO 2-letter code)
# ${4} - English Title
# ${5} - Original Title
# ${6} - Release Date (YYYY-MM-DD)
# ${7} - Watched Date (optional)
# ${8} - Blogger URL (optional)
# ${9} - Photo URL (optional)
#
# Outputs:
# Updates movie.db safely
# Logs errors or debug info
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_SaveMovieRecord() {
local tmdb_id="${1}"
local imdb_id="${2}"
local -r country="${3}"
local title_en="${4}"
local title_orig="${5}"
local -r release_date="${6}"
local -r watched_date="${7}"
local -r blogger_url="${8}"
local -r photo_url="${9}"
local -r file="${MOVIE_DB_PATH}"
local -r FUNC="${FUNCNAME[0]}"
# Sanitize fields
tmdb_id="$(SanitizeField "${tmdb_id}")"
imdb_id="$(SanitizeField "${imdb_id}")"
title_en="$(SanitizeField "${title_en}")"
title_orig="$(SanitizeField "${title_orig}")"
local -r clean_country="$(SanitizeField "${country}")"
local -r clean_release="$(SanitizeField "${release_date}")"
local -r clean_watched="$(SanitizeField "${watched_date}")"
local -r clean_blogger="$(SanitizeField "${blogger_url}")"
local -r clean_photo="$(SanitizeField "${photo_url}")"
# Ensure database file exists
DBMovie_InitMovieDb || {
EchoE "${FUNC}:${LINENO}: Failed to initialize movie.db"
return 1
}
if [[ "${tmdb_id}" == tt* ]]; then
local t="${tmdb_id}"
tmdb_id="${imdb_id}"
imdb_id="${t}"
fi
[[ "${tmdb_id}" == tt* ]] && {
EchoE "${FUNC}:${LINENO}: Invalid TMDB id '${tmdb_id}'"
return 1
}
[[ -n "${imdb_id}" && "${imdb_id}" != tt* ]] && {
EchoE "${FUNC}:${LINENO}: Invalid IMDB id '${imdb_id}'"
return 1
}
[[ -z "${tmdb_id}" && -z "${imdb_id}" ]] && {
EchoE "${FUNC}:${LINENO}: Both TMDB and IMDB are null."
return 1
}
# Remove any existing entry
grep -v -E "^${tmdb_id}\|" "${file}" | grep -v -E "^\w+\|${tmdb_id}\|" > "${file}.tmp" 2>/dev/null || true
# Append new record
echo "${tmdb_id}|${imdb_id}|${clean_country}|${title_en}|${title_orig}|${clean_release}|${clean_watched}|${clean_blogger}|${clean_photo}" >> "${file}.tmp"
# Replace database
mv "${file}.tmp" "${file}" || {
EchoE "${FUNC}:${LINENO}: Failed to update ${file}"
return 1
}
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Saved movie: ${tmdb_id} (${title_en})"
return 0
}
#==============================================================================
# DBMovie_LoadMovieRecordOnline
#
# Fetch a movie's metadata online from TMDb and cache it locally.
#
# Globals used:
# - TMDB_API_KEY
# - MOVIE_DB_PATH
#
# Arguments:
# ${1} - Movie ID (TMDb preferred, or IMDb fallback if needed)
#
# Outputs:
# - Updates movie.db by calling DBMovie_SaveMovieRecord
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_LoadMovieRecordOnline() {
local -r movie_id="$1"
if [[ -z "${movie_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Missing movie ID."
return 1
fi
local movie_json=""
movie_json=$(curl --silent --fail \
"https://api.themoviedb.org/3/movie/${movie_id}?api_key=${TMDB_API_KEY}") || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch movie metadata from TMDb."
return 1
}
local tmdb_id imdb_id country title_en title_orig release_date
tmdb_id=$(jq -r '.id // empty' <<< "${movie_json}")
imdb_id=$(jq -r '.imdb_id // empty' <<< "${movie_json}")
title_en=$(jq -r '.title // empty' <<< "${movie_json}")
title_orig=$(jq -r '.original_title // empty' <<< "${movie_json}")
release_date=$(jq -r '.release_date // empty' <<< "${movie_json}")
country=$(jq -r '.production_countries[0].iso_3166_1 // empty' <<< "${movie_json}")
if [[ -z "${tmdb_id}" || -z "${title_en}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Retrieved movie metadata incomplete."
return 1
fi
export MOVIE_TMDB_ID="${tmdb_id}"
export MOVIE_IMDB_ID="${imdb_id}"
export MOVIE_TITLE_EN="${title_en}"
export MOVIE_TITLE_ORIG="${title_orig}"
DBMovie_SaveMovieRecord "${tmdb_id}" "${imdb_id}" "${country}" "${title_en}" "${title_orig}" "${release_date}" "" "" "" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to save movie record."
return 1
}
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Saved movie to cache: ${title_en} (${release_date}) [${country}]"
return 0
}
#==============================================================================
# FetchMovieMetadataFromTmdb
#
# Fetches and caches movie metadata from TMDb and IMDb for a given movie ID.
# If the movie is already present in the local cache (movie.db), it skips fetch.
#
# Arguments:
# ${1} - TMDb movie ID
#
# Globals used/modified:
# - TMDB_API_KEY
# - DBMovie_SaveMovie
# - DBMovie_HasMovieMetadata
# - MOVIE_DB_PATH (implicitly via DBMovie_* functions)
#
# Dependencies:
# - jq
# - curl
#
# Return:
# 0 - Success
# 1 - Failure or error during TMDb query
#==============================================================================
#FetchMovieMetadataFromTmdb() {
# local -r movie_id="${1}"
# local movie_json imdb_id title original_title release_date country
#
# if [[ -z "${movie_id}" ]]; then
# EchoE "${FUNCNAME[0]}:${LINENO}: Missing movie ID."
# return 1
# fi
#
# if DBMovie_IsMovieCached "${movie_id}"; then
# (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}:✅ Movie ID ${movie_id} already cached."
# return 0
# fi
#
# (( VERBOSE )) && Echo "📡 Fetching movie metadata for TMDb ID ${movie_id}..."
#
# movie_json=$(curl --silent --fail "https://api.themoviedb.org/3/movie/${movie_id}?api_key=${TMDB_API_KEY}")
#
# if [[ -z "${movie_json}" ]]; then
# EchoE "${FUNCNAME[0]}:${LINENO}: Failed to retrieve movie metadata from TMDb."
# return 1
# fi
#
# imdb_id=$(jq -r '.imdb_id // empty' <<< "${movie_json}")
# title=$(jq -r '.title // empty' <<< "${movie_json}")
# original_title=$(jq -r '.original_title // empty' <<< "${movie_json}")
# release_date=$(jq -r '.release_date // empty' <<< "${movie_json}")
# country=$(jq -r '.production_countries[0].iso_3166_1 // empty' <<< "${movie_json}")
#
# DBMovie_SaveMovieRecord "${imdb_id}" "${movie_id}" "${country}" "${title}" "${original_title}" "${release_date}" "" "" ""
#
# (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}:🆕 Movie saved to cache: ${title} (${release_date}) [${country}]"
#
# return 0
#}
#==============================================================================
# DBMovie_LoadMovieRecord
#
# Load a movie record from movie.db by TMDb ID or IMDb ID.
# If not found locally, attempts to fetch the movie metadata online.
#
# Globals used:
# - MOVIE_DB_PATH
# - MOVIE_RECORD_LOADED (exported result)
#
# Arguments:
# ${1} - Movie ID (TMDb preferred, IMDb fallback)
#
# Outputs:
# - Sets MOVIE_RECORD_LOADED
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_LoadMovieRecord() {
local -r movie_id="$(SanitizeField "$1")"
local record=""
local -i nPass=0
local -r FUNC="${FUNCNAME[0]}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: movie_id='${movie_id}'"
if [[ -z "${movie_id}" ]]; then
EchoE "${FUNC}:${LINENO}: Missing movie ID."
return 1
fi
if [[ ! -f "${MOVIE_DB_PATH}" ]]; then
EchoE "${FUNC}:${LINENO}: movie.db not found."
return 1
fi
while [[ ${nPass} -lt 2 ]]; do
# Try TMDb ID first
record=$(grep -E "^${movie_id}\|" "${MOVIE_DB_PATH}") || \
record=$(grep -E "^\w+\|${movie_id}\|" "${MOVIE_DB_PATH}") || true
if [[ -n "${record}" ]]; then
export MOVIE_RECORD_LOADED="${record}"
IFS='|' read -r \
MOVIE_TMDB_ID \
MOVIE_IMDB_ID \
MOVIE_COUNTRY \
MOVIE_TITLE_EN \
MOVIE_TITLE_ORIG \
MOVIE_RELEASE_DATE \
MOVIE_EXISTING_WATCHED_DATE \
MOVIE_BLOGGER_URL \
MOVIE_PHOTO_URL <<< "${record}"
export MOVIE_TMDB_ID
export MOVIE_IMDB_ID
export MOVIE_COUNTRY
export MOVIE_TITLE_EN
export MOVIE_TITLE_ORIG
export MOVIE_RELEASE_DATE
export MOVIE_EXISTING_WATCHED_DATE
export MOVIE_BLOGGER_URL
export MOVIE_PHOTO_URL
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found movie record: ${record}"
echo "${record}"
return 0
fi
if (( nPass == 0 )); then
EchoW "${FUNC}:${LINENO}: Movie ID ${movie_id} not found locally. Attempting online fetch..."
DBMovie_LoadMovieRecordOnline "${movie_id}" || {
EchoE "${FUNC}:${LINENO}: Cannot fetch movie metadata online."
return 1
}
fi
((nPass++))
done
EchoE "${FUNC}:${LINENO}: Movie ID ${movie_id} not found after online fetch."
return 1
}
#==============================================================================
# DBMovie_IsMovieCached
#
# Check if a movie record exists in movie.db by TMDb ID or IMDb ID.
# Tries TMDb ID first (primary key), then IMDb ID (secondary key).
#
# Globals used:
# - MOVIE_DB_PATH
#
# Arguments:
# ${1} - Movie ID (TMDb ID preferred, or IMDb ID as fallback)
#
# Return:
# 0 - Found
# 1 - Not found
#==============================================================================
DBMovie_IsMovieCached() {
local -r movie_id="$1"
local -r file="${MOVIE_DB_PATH}"
if [[ ! -f "${file}" ]]; then
return 1
fi
# First try TMDb ID (primary key match)
if grep -q -E "^${movie_id}\|" "${file}"; then
return 0
fi
# Then try IMDb ID (secondary key match)
if grep -q -E "^\w+\|${movie_id}\|" "${file}"; then
return 0
fi
return 1
}
#==============================================================================
# DBMovie_UpdateWatchedDate
#
# Update the WATCHED_DATE field in movie.db for a given TMDb ID.
# Loads the record, modifies WATCHED_DATE, and saves it via DBMovie_SaveMovieRecord.
#
# Globals used:
# - MOVIE_DB_PATH
#
# Arguments:
# ${1} - TMDb movie ID (primary key)
# ${2} - Watched date (YYYY-MM-DD, optional; defaults to today)
#
# Outputs:
# - Updates movie.db via DBMovie_SaveMovieRecord
# - Logs errors via EchoE
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_UpdateWatchedDate() {
local -r movie_id="${1}"
local -r watched_date="${2:-$(date +%F)}"
local -r file="${MOVIE_DB_PATH}"
if [[ -z "${movie_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Missing TMDb movie ID."
return 1
fi
if [[ ! -f "${file}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: movie.db not found: ${file}"
return 1
fi
# Load the record
local record
record=$(grep -E "^${movie_id}\|" "${file}") || record=$(grep -E "^\w+\|${movie_id}\|" "${file}") || true
if [[ -z "${record}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Movie ID ${movie_id} not found in movie.db."
return 1
fi
# Split fields
IFS='|' read -r tmdb_id imdb_id country title_en title_orig release_date existing_watched_date blogger_url photo_url <<< "${record}"
# Save updated record
DBMovie_SaveMovieRecord "${tmdb_id}" "${imdb_id}" "${country}" "${title_en}" "${title_orig}" "${release_date}" "${watched_date}" "${blogger_url}" "${photo_url}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to save updated movie record."
return 1
}
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Updated WATCHED_DATE for TMDb ID ${tmdb_id} to ${watched_date}"
return 0
}
#==============================================================================
# DBMovie_GetMovieWatchedDate
#
# Retrieve the WATCHED_DATE field from movie.db for a given TMDb or IMDb ID.
# Tries TMDb ID match first, then IMDb ID match.
#
# Globals used:
# - MOVIE_DB_PATH
#
# Arguments:
# ${1} - Movie ID (TMDb preferred, IMDb fallback)
#
# Outputs:
# - Echoes the WATCHED_DATE (YYYY-MM-DD) to stdout
# - Echoes nothing if movie not found
#
# Return:
# 0 - Always
#==============================================================================
DBMovie_GetMovieWatchedDate() {
local -r movie_id="${1}"
local record=""
if [[ -z "${movie_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Missing movie ID."
return 1
fi
if [[ ! -f "${MOVIE_DB_PATH}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: movie.db not found."
return 1
fi
# Try TMDb ID match first
record=$(grep -E "^${movie_id}\|" "${MOVIE_DB_PATH}") || \
record=$(grep -E "^\w+\|${movie_id}\|" "${MOVIE_DB_PATH}") || true
if [[ -n "${record}" ]]; then
IFS='|' read -r _ _ _ _ _ _ watched_date _ _ <<< "${record}"
echo "${watched_date}"
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Found WATCHED_DATE=${watched_date} for ID=${movie_id}"
fi
return 0
}
#==============================================================================
# DBMovie_DeduplicateMovieDb
#
# Cleans up duplicate TMDb or IMDb entries in movie.db.
# Keeps only the latest occurrence for any TMDb or IMDb ID.
#
# Globals used:
# - MOVIE_DB_PATH
#
# Arguments:
# None
#
# Outputs:
# - Overwrites movie.db with deduplicated content
# - Logs progress and errors via Echo/EchoD/EchoE
#
# Return:
# 0 - Success
# 1 - Failure (movie.db not found)
#==============================================================================
DBMovie_DeduplicateMovieDb() {
local -r file="${MOVIE_DB_PATH}"
local -r tmpfile="${file}.dedup.tmp"
if [[ ! -f "${file}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: ❌ movie.db not found: ${file}"
return 1
fi
Echo "${FUNCNAME[0]}:${LINENO}: 🧹 Deduplicating movie.db at ${file}..."
# Preserve header
head -n 1 "${file}" > "${tmpfile}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to copy header to temporary file."
return 1
}
# Deduplicate: prefer last occurrence of TMDb or IMDb ID
tail -n +2 "${file}" | tac | awk -F'|' '
{
tmdb = $1;
imdb = $2;
if (!(tmdb in seen_tmdb) && !(imdb in seen_imdb)) {
lines[NR] = $0;
seen_tmdb[tmdb] = 1;
seen_imdb[imdb] = 1;
}
}
END {
for (i = NR; i >= 1; i--) {
if (i in lines) print lines[i];
}
}
' >> "${tmpfile}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed during deduplication pass."
rm -f "${tmpfile}"
return 1
}
# Replace original file
mv "${tmpfile}" "${file}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to replace original movie.db with deduplicated version."
return 1
}
Echo "${FUNCNAME[0]}:${LINENO}: ✅ movie.db has been deduplicated successfully."
return 0
}
#-----------------------------------------------------------------------------#
# |
# |
# M O V I E C R E D I T |
# |
# |
#-----------------------------------------------------------------------------#
#==============================================================================
# DBMovie_InitMovieCreditsDb
#
# Initializes the movie_credits.db file if missing or empty.
# Creates a header with full Actor and Movie TMDb/IMDb keys plus Role.
#
# Globals:
# MOVIE_CREDITS_DB_PATH - Full path to movie_credits.db
#
# Outputs:
# - Creates the file if missing/empty
# - Logs debug message if DEBUG is enabled
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_InitMovieCreditsDb() {
local -r file="${MOVIE_CREDITS_DB_PATH}"
if [[ ! -s "${file}" ]]; then
mkdir -p "$(dirname "${file}")" 2>/dev/null
if [[ ! -s "${file}" ]]; then
echo "ACTOR_TMDB_ID|ACTOR_IMDB_ID|MOVIE_TMDB_ID|MOVIE_IMDB_ID|ROLE" > "${file}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to create ${file}."
return 1
}
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Created ${file} with header."
fi
fi
return 0
}
#==============================================================================
# DBMovie_SaveMovieCredit
#
# Save or update a movie credit record in movie_credits.db.
# Now stores both TMDb and IMDb IDs for movies and actors.
#
# Globals used:
# - MOVIE_CREDITS_DB_PATH
#
# Arguments:
# ${1} - Movie ID (TMDb or IMDb)
# ${2} - Actor ID (TMDb or IMDb)
# ${3} - Role (character name)
#
# Outputs:
# - Updates movie_credits.db safely
# - Logs debug info via EchoD
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_SaveMovieCredit() {
local -r movie_id="$1"
local -r actor_id="$2"
local role="$3"
local -r file="${MOVIE_CREDITS_DB_PATH}"
local -r FUNC="${FUNCNAME[0]}"
DEBUG=1
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: movie_id=${movie_id} actor_id=${actor_id} "
# Sanitize fields
local -r safe_movie_id="$(SanitizeField "${movie_id}")"
local -r safe_actor_id="$(SanitizeField "${actor_id}")"
local -r safe_role="$(SanitizeField "${role}")"
[[ -z "${safe_movie_id}" || -z "${safe_actor_id}" ]] && {
EchoE "${FUNC}:${LINENO}: Missing movie ID or actor ID."
return 1
}
# Ensure database file exists
DBMovie_InitMovieCreditsDb || {
EchoE "${FUNC}:${LINENO}: Failed to initialize movie_credits.db"
return 1
}
# Resolve all 4 IDs
local actor_tmdb=""
local actor_imdb=""
local movie_tmdb=""
local movie_imdb=""
# Lookup movie
local -r movie_record="$(DBMovie_LoadMovieRecord "${safe_movie_id}")" || {
EchoE "${FUNC}:${LINENO}: Movie ID '${safe_movie_id}' not found."
return 1
}
[[ -z "${movie_record}" ]] && {
EchoE "${FUNC}:${LINENO}: movie_record empty."
return 1
}
IFS='|' read -r movie_tmdb movie_imdb _ <<< "${movie_record}"
[[ -z "${movie_tmdb}" && -z "${movie_imdb}" ]] && {
EchoE "${FUNC}:${LINENO}: Movie ID not defined after movie_record load."
return 1
}
# Lookup actor
local -r actor_record="$(DBMovie_LoadActorRecord "${safe_actor_id}")" || {
EchoE "${FUNC}:${LINENO}: Actor ID '${safe_actor_id}' not found."
return 1
}
IFS='|' read -r actor_tmdb actor_imdb _ <<< "${actor_record}"
# Remove existing record matching Actor + Movie
awk -F'|' -v at="${actor_tmdb}" -v ai="${actor_imdb}" -v mt="${movie_tmdb}" -v mi="${movie_imdb}" '{
if (!($1 == at && $2 == ai && $3 == mt && $4 == mi)) print $0;
}' "${file}" > "${file}.tmp" 2>/dev/null || true
# Save new record
echo "${actor_tmdb}|${actor_imdb}|${movie_tmdb}|${movie_imdb}|${safe_role}" >> "${file}.tmp"
# Replace database
mv "${file}.tmp" "${file}" || {
EchoE "${FUNC}:${LINENO}: Failed to update movie_credits.db"
return 1
}
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Saved movie credit: Actor ${actor_tmdb}/${actor_imdb} Movie ${movie_tmdb}/${movie_imdb}"
return 0
}
#==============================================================================
# DBMovie_LoadMovieCreditByActorIdOnline
#
# Fetches an actor's filmography from TMDb and stores records using
# DBMovie_SaveMovieCredit. Accepts either TMDb or IMDb actor ID.
#
# Arguments:
# $1 - Actor ID (TMDb numeric or IMDb ID like "nm0000001")
#
# Globals Used:
# - ACTOR_TMDB_ID
# - ACTOR_IMDB_ID
# - TMDB_API_KEY
#
# Returns:
# 0 on success, 1 on failure
#==============================================================================
DBMovie_LoadMovieCreditByActorIdOnline() {
local -r actor_id_input="${1:-}"
local -r FUNC="${FUNCNAME[0]}"
if [[ -z "${actor_id_input}" ]]; then
EchoE "${FUNC}:${LINENO}: No actor ID provided."
return 1
fi
ResolveActorIds "${actor_id_input}" || {
EchoE "${FUNC}:${LINENO}: Failed to resolve actor ID '${actor_id_input}'"
return 1
}
local -r tmdb_id="${ACTOR_TMDB_ID}"
local -r imdb_id="${ACTOR_IMDB_ID}"
DBMovie_InitMovieCreditsDb || return 1
local -r url="https://api.themoviedb.org/3/person/${tmdb_id}/movie_credits?api_key=${TMDB_API_KEY}"
local -r credits_json="$(curl --silent --fail "${url}")" || {
EchoE "${FUNC}:${LINENO}: Failed to fetch movie credits for actor ${tmdb_id} from TMDb."
return 1
}
Echo "Retriving all movies with actor '${ACTOR_TMDB_ID}'. Use --verbose to see ach movie title."
local -i count=0
while read -r entry; do
local movie_tmdb_id="$(jq -r '.id // empty' <<< "${entry}")"
local role="$(jq -r '.character // empty' <<< "${entry}")"
[[ -z "${movie_tmdb_id}" || -z "${role}" ]] && continue
# Fetch metadata (will automatically cache)
if DBMovie_LoadMovieRecord "${movie_tmdb_id}" >/dev/null; then
(( VERBOSE )) && Echo " 🎬 Saving movie credit for movie '${MOVIE_TMDB_ID}' '${MOVIE_TITLE_EN} (${MOVIE_RELEASE_DATE})'"
DBMovie_SaveMovieCredit "${movie_tmdb_id}" "${tmdb_id}" "${role}" || {
EchoW "${FUNC}:${LINENO}: Failed to save credit for movie ${movie_tmdb_id}"
}
((count++))
else
EchoW "${FUNC}:${LINENO}: Could not fetch metadata for movie ID ${movie_tmdb_id}"
fi
done < <(
jq -c '.cast[] | select(.id and .title and .character)' <<< "${credits_json}"
)
if (( count == 0 )); then
EchoW "${FUNC}:${LINENO}: No usable credits saved for actor ${tmdb_id}"
return 1
fi
Echo "📚 Saved ${count} movie credit(s) for actor ${tmdb_id} (${imdb_id})"
return 0
}
#==============================================================================
# DBMovie_LoadMovieCreditByActorId
#
# Returns all movie credits for a given actor from the local movie_credits.db.
# If no record is found, attempts to fetch and cache via TMDb API.
#
# Arguments:
# $1 - Actor TMDb ID or IMDb ID
#
# Globals Used:
# ACTOR_TMDB_ID
# MOVIE_CREDITS_DB_PATH
#
# Output:
# Matching lines from movie_credits.db where actor_id == resolved TMDb ID
#
# Returns:
# 0 on success, 1 on error
#==============================================================================
DBMovie_LoadMovieCreditByActorId() {
local -r actor_id_input="$(SanitizeField "${1:-}")"
local -r FUNC="${FUNCNAME[0]}"
DBMovie_InitMovieCreditsDb || return 1
if [[ -z "${actor_id_input}" ]]; then
EchoE "${FUNC}:${LINENO}: No actor ID provided."
return 1
fi
ResolveActorIds "${actor_id_input}" || {
EchoE "${FUNC}:${LINENO}: Failed to resolve TMDb ID for '${actor_id_input}'"
return 1
}
for (( i=0; i<2; i++ )); do
# TMDB id is the first field
local result="$(grep "^${ACTOR_TMDB_ID}|" "${MOVIE_CREDITS_DB_PATH}" || true)"
if [[ -n "${result}" ]]; then
echo "${result}"
return 0
fi
if (( i == 0 )); then
EchoW "${FUNC}:${LINENO}: No local movie credits found for actor ${ACTOR_TMDB_ID}, fetching online..."
DBMovie_LoadMovieCreditByActorIdOnline "${actor_id_input}" >/dev/null || {
EchoW "${FUNC}:${LINENO}: Online fetch failed."
break
}
fi
done
EchoE "${FUNC}:${LINENO}: No movie credits found for actor ${actor_id_input} after cache and online attempts."
return 1
}
#==============================================================================
# DBMovie_LoadMovieCreditByMovieId
#
# Returns all actor credits for a given movie from the local movie_credits.db.
#
# Arguments:
# $1 - Movie TMDb ID or IMDb ID
#
# Globals Used:
# MOVIE_CREDITS_DB_PATH
# ACTOR_TMDB_ID (not used here)
#
# Output:
# Matching lines from movie_credits.db where movie_id == resolved TMDb ID
#
# Returns:
# 0 on success, 1 on error
#==============================================================================
DBMovie_LoadMovieCreditByMovieId() {
local -r movie_id_input="${1:-}"
local -r db_file="${MOVIE_CREDITS_DB_PATH}"
local -r FUNC="${FUNCNAME[0]}"
DBMovie_InitMovieCreditsDb || {
EchoE "${FUNC}:${LINENO}: Failed to initialize ${db_file}"
return 1
}
if [[ -z "${movie_id_input}" ]]; then
EchoE "${FUNC}:${LINENO}: No movie ID provided."
return 1
fi
# Resolve to TMDb ID
local tmdb_id=""
if [[ "${movie_id_input}" =~ ^tt[0-9]+$ ]]; then
tmdb_id="$(ResolveMovieTmdbIdFromImdb "${movie_id_input}")" || {
EchoE "${FUNC}:${LINENO}: Failed to resolve TMDb ID for IMDb ID '${movie_id_input}'"
return 1
}
else
tmdb_id="${movie_id_input}"
if ! DBMovie_HasMovieMetadata "${tmdb_id}"; then
EchoW "${FUNC}:${LINENO}: Movie ID ${tmdb_id} not in cache. Attempting fetch..."
DBMovie_LoadMovieRecord "${tmdb_id}" >/dev/null || {
EchoE "${FUNC}:${LINENO}: Could not fetch or resolve movie metadata."
return 1
}
fi
fi
local result=""
result="$(awk -F'|' -v mid="${tmdb_id}" '$3 == mid { print $0 }' "${db_file}" || true)"
if [[ -n "${result}" ]]; then
echo "${result}"
return 0
else
EchoW "${FUNC}:${LINENO}: No credits found for movie ID ${tmdb_id}"
return 1
fi
}
#==============================================================================
# DBMovie_DeleteCreditsByActor
#
# Remove all movie credit entries for a given actor (by TMDb or IMDb ID).
#
# Globals used:
# - MOVIE_CREDITS_DB_PATH
#
# Arguments:
# ${1} - Actor ID (can be TMDb or IMDb)
#
# Outputs:
# - Updates movie_credits.db safely
# - Logs debug info via EchoD
#
# Return:
# 0 - Always (even if no matching entries found)
#==============================================================================
DBMovie_DeleteCreditsByActor() {
local -r actor_id="$(SanitizeField "${1}")"
local -r FUNC="${FUNCNAME[0]}"
if [[ -z "${actor_id}" ]]; then
EchoE "${FUNC}:${LINENO}: Missing actor ID."
return 1
fi
DBMovie_InitMovieCreditsDb || return 1
# Load actor record to resolve TMDb and IMDb IDs
local actor_record="$(DBMovie_LoadActorRecord "${actor_id}")" || {
EchoE "${FUNC}:${LINENO}: Cannot resolve actor ID '${actor_id}'."
return 1
}
IFS='|' read -r actor_tmdb_id actor_imdb_id _ <<< "${actor_record}"
if [[ -z "${actor_tmdb_id}" && -z "${actor_imdb_id}" ]]; then
EchoE "${FUNC}:${LINENO}: Resolved actor IDs are empty."
return 1
fi
# Filter: Remove any line where (Actor_TMDB_ID == tmdb) or (Actor_IMDB_ID == imdb)
awk -F'|' -v at="${actor_tmdb_id}" -v ai="${actor_imdb_id}" '{
if (!($1 == at || $2 == ai)) print $0;
}' "${MOVIE_CREDITS_DB_PATH}" > "${MOVIE_CREDITS_DB_PATH}.tmp" 2>/dev/null || true
mv "${MOVIE_CREDITS_DB_PATH}.tmp" "${MOVIE_CREDITS_DB_PATH}" || {
EchoE "${FUNC}:${LINENO}: Failed to update ${MOVIE_CREDITS_DB_PATH}"
return 1
}
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Deleted movie credits for actor ${actor_tmdb_id}/${actor_imdb_id}."
return 0
}
#==============================================================================
# DBMovie_IsCreditCached
#
# Check if a movie credit exists inside movie_credits.db.
# Accepts TMDb or IMDb IDs for both movie and actor.
#
# Globals used:
# - MOVIE_CREDITS_DB_PATH
#
# Arguments:
# ${1} - Movie ID (TMDb or IMDb)
# ${2} - Actor ID (TMDb or IMDb)
#
# Return:
# 0 - Credit found
# 1 - Not found or error
#==============================================================================
DBMovie_IsCreditCached() {
local -r movie_id="$1"
local -r actor_id="$2"
local -r file="${MOVIE_CREDITS_DB_PATH}"
if [[ -z "${movie_id}" || -z "${actor_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Missing movie or actor ID."
return 1
fi
if [[ ! -f "${file}" ]]; then
return 1
fi
# Resolve full movie and actor records
local movie_record actor_record
movie_record="$(DBMovie_LoadMovieRecord "${movie_id}")" || return 1
actor_record="$(DBMovie_LoadActorRecord "${actor_id}")" || return 1
local movie_tmdb movie_imdb actor_tmdb actor_imdb
IFS='|' read -r movie_tmdb movie_imdb _ <<< "${movie_record}"
IFS='|' read -r actor_tmdb actor_imdb _ <<< "${actor_record}"
# Search credit: match either TMDb or IMDb IDs
awk -F'|' -v at="${actor_tmdb}" -v ai="${actor_imdb}" -v mt="${movie_tmdb}" -v mi="${movie_imdb}" '{
if (($1 == at || $2 == ai) && ($3 == mt || $4 == mi)) {
exit 0
}
}
END { exit 1 }
' "${file}"
}
#==============================================================================
# DBMovie_LoadMoviesByActor
#
# Outputs all movie.db lines corresponding to a given actor (TMDb or IMDb ID).
# Resolves IDs using cache first. Ensures safe output, with full matching lines.
#
# Globals used:
# - MOVIE_DB_PATH
# - MOVIE_CREDITS_DB_PATH
#
# Arguments:
# ${1} - Actor ID (TMDb or IMDb)
#
# Outputs:
# Echoes full lines from movie.db to stdout
#
# Return:
# 0 - Success (even if no movies found)
# 1 - Failure (errors, missing inputs, missing cache)
#==============================================================================
DBMovie_LoadMoviesByActor() {
local -r actor_id="$1"
local -r tmp_ids="$(mktemp)"
if [[ -z "${actor_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Missing actor ID."
rm -f "${tmp_ids}"
return 1
fi
DBMovie_InitMovieDb
DBMovie_InitMovieCreditsDb
# Resolve actor IDs
local actor_record=""
actor_record="$(DBMovie_LoadActorRecord "${actor_id}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Actor '${actor_id}' not found in cache."
rm -f "${tmp_ids}"
return 1
}
# "ACTOR_TMDB_ID|ACTOR_IMDB_ID|MOVIE_TMDB_ID|MOVIE_IMDB_ID|ROLE"
local actor_tmdb_id=""
local actor_imdb_id=""
IFS='|' read -r actor_tmdb_id actor_imdb_id _ <<< "${actor_record}"
# Extract matching movie IDs (TMDb or IMDb)
awk -F'|' -v at="${actor_tmdb_id}" -v ai="${actor_imdb_id}" '{
if ($1 == at || $2 == ai) print $3;
}' "${MOVIE_CREDITS_DB_PATH}" > "${tmp_ids}"
if [[ ! -s "${tmp_ids}" ]]; then
EchoW "${FUNCNAME[0]}:${LINENO}: No movie credits found for actor ${actor_tmdb_id}/${actor_imdb_id}."
rm -f "${tmp_ids}"
return 0
fi
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Extracted movie IDs: $(< "${tmp_ids}")"
# Output matching full movie.db lines
awk -F'|' 'NR==FNR { ids[$1]; next } $1 in ids' "${tmp_ids}" "${MOVIE_DB_PATH}"
rm -f "${tmp_ids}"
return 0
}
#==============================================================================
# DBMovie_GetMovieRole
#
# Retrieves the role an actor played in a specific movie using movie_credits.db.
# Accepts either TMDb or IMDb IDs for both actor and movie.
#
# Format of each line in movie_credits.db:
# ACTOR_TMDB_ID|ACTOR_IMDB_ID|MOVIE_TMDB_ID|MOVIE_IMDB_ID|ROLE
#
# Arguments:
# $1 - Movie ID (TMDb numeric or IMDb ID like "tt...")
# $2 - Actor ID (TMDb numeric or IMDb ID like "nm...")
#
# Globals Used:
# - MOVIE_CREDITS_DB_PATH
#
# Output:
# - Role string to stdout (only if found and unambiguous)
#
# Returns:
# 0 - success (role found and unique if both IDs given)
# 1 - failure (missing ID, resolution error, or ambiguity)
#==============================================================================
DBMovie_GetMovieRole() {
local -r FUNC="${FUNCNAME[0]}"
local -r movie_input="$(SanitizeField "${1}")"
local -r actor_input="$(SanitizeField "${2}")"
if [[ -z "${movie_input}" || -z "${actor_input}" ]]; then
EchoE "${FUNC}:${LINENO}: Both movie ID and actor ID must be provided."
return 1
fi
DBMovie_InitMovieCreditsDb || return 1
# Resolve movie and actor TMDb IDs
local movie_record actor_record movie_tmdb actor_tmdb
movie_record="$(DBMovie_LoadMovieRecord "${movie_input}")" || {
EchoE "${FUNC}:${LINENO}: Could not resolve movie ID '${movie_input}'"
return 1
}
actor_record="$(DBMovie_LoadActorRecord "${actor_input}")" || {
EchoE "${FUNC}:${LINENO}: Could not resolve actor ID '${actor_input}'"
return 1
}
IFS='|' read -r movie_tmdb _ _ <<< "${movie_record}"
IFS='|' read -r actor_tmdb _ _ <<< "${actor_record}"
# Match all lines where actor and/or movie match
local -a matches=()
local line a_tmdb a_imdb m_tmdb m_imdb role
while IFS='|' read -r a_tmdb a_imdb m_tmdb m_imdb role; do
if [[ "${a_tmdb}" == "${actor_tmdb}" && "${m_tmdb}" == "${movie_tmdb}" ]]; then
matches+=("${role}")
fi
done < "${MOVIE_CREDITS_DB_PATH}"
if (( ${#matches[@]} == 1 )); then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: Found role for actor ${actor_tmdb} in movie ${movie_tmdb}: ${matches[0]}"
echo "${matches[0]}"
return 0
elif (( ${#matches[@]} == 0 )); then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: No matching credit found."
return 1
else
EchoE "${FUNC}:${LINENO}: Multiple entries found for actor ${actor_tmdb} and movie ${movie_tmdb} — expected only one."
return 1
fi
}
#==============================================================================
# DBMovie_BuildActorFilmographyTable: Build HTML table rows for actor filmography
#------------------------------------------------------------------------------
# Description:
# Generates the HTML table rows for an actor's filmography by retrieving
# movie credits from TMDb using the person/<id>/movie_credits endpoint. For
# each movie role, it computes the actor's age at release, checks whether
# the movie was released after the actor's death (marks ❌), and builds a
# complete <tr> element with:
# - Release year
# - Title
# - Role name
# - Calculated age
# - Link to IMDb search for the title
#
# The generated HTML table is returned as a single string via stdout.
#
# Arguments:
# ${1} - TMDb actor ID (e.g., 12345)
#
# Globals Used:
# ACTOR_TMDB_ID - TMDb actor ID (reference only)
# ACTOR_BIRTH_YEAR - Year of birth (YYYY)
# ACTOR_DOD - Date of death (YYYY-MM-DD or empty)
# TMDB_API_KEY - Required TMDb API key
# MOVIE_DB_PATH - Path to local movie metadata file (not used here but assumed related)
#
# Outputs:
# Echoes a complete HTML <table> element with one row per film
#
# Return:
# 0 - Always
#==============================================================================
DBMovie_BuildActorFilmographyTable() {
local -r tmdb_id="${1}"
local output=""
local -r tmpfile=$(mktemp)
local -r credits_url="https://api.themoviedb.org/3/person/${tmdb_id}/movie_credits?api_key=${TMDB_API_KEY}"
curl --silent --fail "${credits_url}" > "${tmpfile}"
output+="<h2>Filmography</h2>"
output+="<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">"
output+="<tr><th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th></tr>"
while IFS='|' read -r release title role; do
year="${release:0:4}"
age="?"
if [[ "${ACTOR_BIRTH_YEAR}" =~ ^[0-9]+$ && "${year}" =~ ^[0-9]+$ ]]; then
age=$((year - ACTOR_BIRTH_YEAR))
fi
if [[ -n "${ACTOR_DOD}" && "${year}" -gt "${ACTOR_DOD:0:4}" ]]; then
age='<span style="color:red;">❌</span>'
fi
local search_url="https://www.imdb.com/find?q=$(urlencode "${title} ${year}")"
output+="<tr><td>${year}</td><td>${title}</td><td>${role}</td><td>${age}</td><td><a href=\"${search_url}\" target=\"_blank\">🔍</a></td></tr>"
done < <(
jq -r '.cast[] | select(.release_date != null and .title != null) |
[.release_date, .title, .character] | @tsv' "${tmpfile}"
)
output+="</table>"
rm -f "${tmpfile}"
echo "${output}"
}
#-----------------------------------------------------------------------------#
# |
# |
# L I N K |
# |
# |
#-----------------------------------------------------------------------------#
#==============================================================================
# DBMovie_InitLinksDb
#
# Initialize links.db if it does not exist or is empty.
# Creates the file with the proper header line for actor/movie/credit links.
#
# Globals used:
# - LINKS_DB_PATH
#
# Globals modified:
# - links.db (created if missing)
#
# Arguments:
# None
#
# Outputs:
# - Creates the links.db file if missing or empty
# - Logs debug message on successful creation if DEBUG is enabled
#
# Return:
# 0 - Success
# 1 - Failure (unable to create file)
#==============================================================================
DBMovie_InitLinksDb() {
local -r file="${LINKS_DB_PATH}"
if [[ ! -s "${file}" ]]; then
mkdir -p "$(dirname "${file}")" 2>/dev/null
if [[ ! -s "${file}" ]]; then
echo "TYPE|ACTOR_TMDB_ID|ACTOR_IMDB_ID|MOVIE_TMDB_ID|MOVIE_IMDB_ID|URL" > "${file}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to create ${file}."
return 1
}
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Created ${file} with header."
fi
fi
return 0
}
#==============================================================================
# DBMovie_SaveLinkRecord
#
# Save or update a link entry inside links.db.
# Resolves missing TMDb/IMDb IDs automatically from local caches.
#
# Globals used:
# - MOVIE_LINKS_DB_PATH
# - ACTOR_DB_PATH
# - MOVIE_DB_PATH
#
# Arguments:
# ${1} - Type (A, M, C)
# ${2} - TMDb Actor ID (may be empty)
# ${3} - IMDb Actor ID (may be empty)
# ${4} - TMDb Movie ID (may be empty)
# ${5} - IMDb Movie ID (may be empty)
# ${6} - Full Encoded URL
#
# Outputs:
# Writes to ${MOVIE_LINKS_DB_PATH}
# Logs debug info if DEBUG is enabled
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
DBMovie_SaveLinkRecord() {
local -r type="${1}"
local tmdb_actor="${2}"
local imdb_actor="${3}"
local tmdb_movie="${4}"
local imdb_movie="${5}"
local url="${6}"
local -r file="${MOVIE_LINKS_DB_PATH}"
# Sanitize URL only
sanitize() { echo "${1//[$'\t\r\n|']}"; }
url="$(sanitize "${url}")"
if [[ -z "${type}" || -z "${url}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Missing type or URL."
return 1
fi
# Resolve missing IDs
case "${type}" in
A)
if [[ -z "${tmdb_actor}" && -z "${imdb_actor}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Actor ID (TMDb or IMDb) required for Actor link."
return 1
fi
if [[ -z "${tmdb_actor}" ]]; then
local record
record="$(DBMovie_LoadActorRecord "${imdb_actor}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve TMDb ID from IMDb ID '${imdb_actor}'."
return 1
}
IFS='|' read -r tmdb_actor imdb_actor _ <<< "${record}"
elif [[ -z "${imdb_actor}" ]]; then
local record
record="$(DBMovie_LoadActorRecord "${tmdb_actor}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve IMDb ID from TMDb ID '${tmdb_actor}'."
return 1
}
IFS='|' read -r tmdb_actor imdb_actor _ <<< "${record}"
fi
tmdb_movie=""
imdb_movie=""
;;
M)
if [[ -z "${tmdb_movie}" && -z "${imdb_movie}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Movie ID (TMDb or IMDb) required for Movie link."
return 1
fi
if [[ -z "${tmdb_movie}" ]]; then
local record
record="$(DBMovie_LoadMovieRecord "${imdb_movie}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve TMDb ID from IMDb ID '${imdb_movie}'."
return 1
}
IFS='|' read -r tmdb_movie imdb_movie _ <<< "${record}"
elif [[ -z "${imdb_movie}" ]]; then
local record
record="$(DBMovie_LoadMovieRecord "${tmdb_movie}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve IMDb ID from TMDb ID '${tmdb_movie}'."
return 1
}
IFS='|' read -r tmdb_movie imdb_movie _ <<< "${record}"
fi
tmdb_actor=""
imdb_actor=""
;;
C)
if [[ (-z "${tmdb_actor}" && -z "${imdb_actor}") || (-z "${tmdb_movie}" && -z "${imdb_movie}") ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Both actor and movie IDs required for Credit link."
return 1
fi
# Resolve actor IDs
if [[ -z "${tmdb_actor}" ]]; then
local record
record="$(DBMovie_LoadActorRecord "${imdb_actor}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve actor TMDb ID from IMDb ID '${imdb_actor}'."
return 1
}
IFS='|' read -r tmdb_actor imdb_actor _ <<< "${record}"
elif [[ -z "${imdb_actor}" ]]; then
local record
record="$(DBMovie_LoadActorRecord "${tmdb_actor}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve actor IMDb ID from TMDb ID '${tmdb_actor}'."
return 1
}
IFS='|' read -r tmdb_actor imdb_actor _ <<< "${record}"
fi
# Resolve movie IDs
if [[ -z "${tmdb_movie}" ]]; then
local record
record="$(DBMovie_LoadMovieRecord "${imdb_movie}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve movie TMDb ID from IMDb ID '${imdb_movie}'."
return 1
}
IFS='|' read -r tmdb_movie imdb_movie _ <<< "${record}"
elif [[ -z "${imdb_movie}" ]]; then
local record
record="$(DBMovie_LoadMovieRecord "${tmdb_movie}")" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Cannot resolve movie IMDb ID from TMDb ID '${tmdb_movie}'."
return 1
}
IFS='|' read -r tmdb_movie imdb_movie _ <<< "${record}"
fi
;;
*)
EchoE "${FUNCNAME[0]}:${LINENO}: Invalid link type '${type}'. Must be A, M, or C."
return 1
;;
esac
# Ensure links database is initialized
DBMovie_InitLinksDb || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to initialize links database."
return 1
}
# Remove any existing identical entry
grep -v -F "|${url}" "${file}" > "${file}.tmp" 2>/dev/null || true
# Append the new record
echo "${type}|${tmdb_actor}|${imdb_actor}|${tmdb_movie}|${imdb_movie}|${url}" >> "${file}.tmp"
mv "${file}.tmp" "${file}"
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Saved link: Type=${type}, URL=${url}."
return 0
}
#==============================================================================
# DBMovie_SortUniqueLinks
#
# Sort and remove duplicate entries inside links.db.
# Preserves the header, sorts the rest uniquely.
#
# Globals used:
# - LINKS_DB_PATH
# Globals modified:
# - links.db (rewritten in place)
#
# Arguments:
# None
#
# Outputs:
# - Overwrites links.db sorted and deduplicated
# - Logs debug message if DEBUG is enabled
#
# Return:
# 0 - Always returns 0
#==============================================================================
DBMovie_SortUniqueLinks() {
local -r file="${LINKS_DB_PATH}"
# Always ensure database is initialized first
DBMovie_InitLinksDb || {
EchoE "${FUNCNAME[0]}:${LINENO}: Failed to initialize links database."
return 1
}
local -r tmpfile="${file}.tmp"
# Sort and deduplicate while preserving the header
{
head -n 1 "${file}"
tail -n +2 "${file}" | sort -u
} > "${tmpfile}"
mv "${tmpfile}" "${file}"
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: links.db sorted and deduplicated successfully."
return 0
}
#-----------------------------------------------------------------------------#
# |
# |
# O T H E R |
# |
# |
#-----------------------------------------------------------------------------#
#==============================================================================
# DBMovie_InitDb
#
# Initialize all local movie database files (actor.db, movie.db, links.db,
# movie_credits.db). Ensures they exist and have correct headers.
#
# Globals used:
# - ACTOR_DB_PATH
# - MOVIE_DB_PATH
# - LINKS_DB_PATH
# - MOVIE_CREDITS_DB_PATH
#
# Globals modified:
# - Creates or updates all .db files as needed
#
# Arguments:
# None
#
# Return:
# 0 - All databases initialized successfully
# 1 - Failure to initialize one or more databases
#==============================================================================
DBMovie_InitDb() {
if DBMovie_InitActorDb \
&& DBMovie_InitMovieDb \
&& DBMovie_InitLinksDb \
&& DBMovie_InitMovieCreditsDb; then
return 0
else
EchoE "${FUNCNAME[0]}:${LINENO}: Not possible to initialize the movie database."
return 1
fi
}
#***************************************************************************************
#***************************************************************************************
#***************************************************************************************
#***************************************************************************************
#==============================================================================
# HandleActorMode
#
# Handle operations related to actor-based post creation and metadata update.
#
# Globals used:
# - FORCE_ACTOR_RESCAN
# - DRY_RUN
# - VERBOSE
#
# Globals modified:
# - ACTOR_NAME
# - ACTOR_TMDB_ID
# - ACTOR_IMDB_ID
#
# Arguments:
# ${1} - Actor name or IMDb ID
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
# HandleActorMode() {
# local -r actor_input="$1"
#
# if [[ "${FORCE_ACTOR_RESCAN}" -eq 1 ]]; then
# Echo "🔄 Force-rescanning actor '${actor_input}'..."
# FetchActorListFromTmdb "${actor_input}" || {
# EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch actor list."
# return 1
# }
# else
# ResolveActorIds "${actor_input}" || {
# EchoE "${FUNCNAME[0]}:${LINENO}: Failed to resolve actor IDs."
# return 1
# }
# fi
#
# DBMovie_LoadActorRecordParse "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" || {
# EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch actor metadata."
# return 1
# }
#
# DBMovie_LoadMovieCreditByActorId "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" || {
# EchoE "${FUNCNAME[0]}:${LINENO}: Failed to fetch movie credits."
# return 1
# }
#
# GenerateActorBlogPost "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" || {
# EchoE "${FUNCNAME[0]}:${LINENO}: Failed to generate blog post."
# return 1
# }
#
# return 0
# }
#==============================================================================
# HandleMovieMode
#
# Handle operations related to movie-based metadata fetching and blog post creation.
#
# Globals used:
# - DRY_RUN
# - VERBOSE
#
# Globals modified:
# - None
#
# Arguments:
# ${1} - Movie name or IMDb ID
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
# HandleMovieMode() {
# local -r movie_input="$1"
#
# EchoE "${FUNCNAME[0]}:${LINENO}: Movie handling not implemented yet."
# return 1
# }
#==============================================================================
# EnsureDependency: Ensure tool is available via install, fallback, or error
#------------------------------------------------------------------------------
# Description:
# Attempts to locate a required CLI tool. If not found, attempts to install
# it based on the current OS:
# - Cygwin: runs local setup-x86_64.exe with -P <package>
# - Debian/Ubuntu: uses apt-get
# - macOS: uses brew
#
# If install fails or is unsupported, falls back to a named function if given.
#
# Arguments:
# $1 - Tool name (e.g. "jq")
# $2 - Package name (same as tool or alternative)
# $3 - Fallback function name (optional)
#
# Globals:
# DEBUG (controls debug output)
# Outputs:
# - Echo status messages
# - Defines fallback function if install fails and fallback is provided
#==============================================================================
EnsureDependency() {
local -r tool="${1}"
local -r package="${2}"
local -r fallback_func="${3:-}"
if command -v "${tool}" &>/dev/null; then
(( DEBUG )) && EchoD "[EnsureDependency] '${tool}' found"
return 0
fi
EchoW "Missing tool: '${tool}'. Attempting to install package: ${package}..."
case "$(uname -s)" in
CYGWIN*)
local installer="/setup-x86_64.exe"
[[ -x "${installer}" ]] || installer="/cygdrive/f/Downloads/setup-x86_64_2.932.exe"
if [[ -x "${installer}" ]]; then
"${installer}" -q -P "${package}" &>/dev/null
else
EchoW "Cygwin installer not found. Please install '${package}' manually."
fi
;;
Linux)
if command -v apt-get &>/dev/null; then
sudo apt-get update && sudo apt-get install -y "${package}"
fi
;;
Darwin)
if command -v brew &>/dev/null; then
brew install "${package}"
fi
;;
*)
EchoW "Unsupported platform. Cannot install '${package}' automatically."
;;
esac
if command -v "${tool}" &>/dev/null; then
Echo "✔ Tool '${tool}' is now available."
return 0
elif [[ -n "${fallback_func}" ]]; then
EchoW "Tool '${tool}' still missing. Attempting fallback: '${fallback_func}'"
"${fallback_func}"
return $?
else
ExitWithMsg "Required tool '${tool}' could not be installed."
fi
}
#==============================================================================
# DefineUrlencodeFallback: Define a fallback implementation for 'urlencode'
#------------------------------------------------------------------------------
# Description:
# Creates a shell function named `urlencode` to encode a string for safe use
# in URLs if it is not already defined. This fallback is only used when a
# native or installed `urlencode` command is unavailable.
#
# Priority is given to the following implementations:
# 1. Python 3 (urllib.parse.quote)
# 2. Perl (URI::Escape)
# 3. Pure Bash fallback using percent encoding
#
# This function should be called during dependency checks when
# --check-deps-local or --check-deps-install is active.
#
# Globals Used:
# DEBUG - If set, logs debug output on existing function
#
# Outputs:
# - Defines a shell function named `urlencode`
# - Logs which method was used for fallback definition
#
# Return:
# 0 - Always
#==============================================================================
DefineUrlencodeFallback() {
if declare -F urlencode &>/dev/null; then
(( DEBUG )) && EchoD "'urlencode' already defined, skipping fallback setup"
elif command -v python3 &>/dev/null; then
urlencode() {
python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$1"
}
EchoW "Defined fallback 'urlencode' using python3"
elif command -v perl &>/dev/null; then
urlencode() {
perl -MURI::Escape -e 'print uri_escape($ARGV[0]);' "$1"
}
EchoW "Defined fallback 'urlencode' using perl"
else
urlencode() {
local LANG=C
local string="${1}"
local length="${#string}"
local i
for (( i = 0; i < length; i++ )); do
local c="${string:$i:1}"
case "${c}" in
[a-zA-Z0-9.~_-]) printf "%s" "${c}" ;;
*) printf "%%%02X" "'${c}" ;;
esac
done
}
EchoW "Defined fallback 'urlencode' using pure bash"
fi
return 0
}
#==============================================================================
# GetBlogAccessToken: Retrieve a Blogger API access token using a refresh token
#------------------------------------------------------------------------------
# Description:
# Obtains a new access token from the Google OAuth 2.0 endpoint using the
# configured refresh token. This function retries every few seconds until
# a valid token is obtained and exported.
#
# It uses the `curl` command to send a POST request and parses the JSON
# response to extract `.access_token` using `jq`. If the token is invalid
# or the request fails, the function sleeps for a fixed interval and retries.
#
# Globals Used:
# GOOGLE_CLIENT_ID - OAuth 2.0 client ID
# GOOGLE_CLIENT_SECRET - OAuth 2.0 client secret
# GOOGLE_REFRESH_TOKEN - Refresh token obtained via initial authorization
# DEBUG - If set, logs additional diagnostics
#
# Globals Set:
# BLOG_ACCESS_TOKEN - The newly retrieved Blogger API access token (exported)
#
# Outputs:
# - Echoes status messages and errors to the console
# - Logs detailed retry messages if the request fails
#
# Return:
# 0 - Once a valid access token is retrieved
#==============================================================================
GetBlogAccessToken() {
Echo "Obtaining Blog Access Token..."
declare -g BLOG_ACCESS_TOKEN=""
local -i attempt=0
local -r MAX_WAIT=2
while :; do
(( attempt++ ))
local BLOG_TOKEN_RESPONSE
BLOG_TOKEN_RESPONSE=$(curl --silent --request POST \
--data "client_id=${GOOGLE_CLIENT_ID}" \
--data "client_secret=${GOOGLE_CLIENT_SECRET}" \
--data "refresh_token=${GOOGLE_REFRESH_TOKEN}" \
--data "grant_type=refresh_token" \
https://oauth2.googleapis.com/token)
BLOG_ACCESS_TOKEN=$(jq -r '.access_token' <<<"${BLOG_TOKEN_RESPONSE}")
if [[ -n "${BLOG_ACCESS_TOKEN}" && "${BLOG_ACCESS_TOKEN}" != "null" ]]; then
(( DEBUG )) && EchoD "Access token retrieved on attempt #${attempt}"
break
fi
EchoE "Failed to retrieve a valid access token. Retrying in ${MAX_WAIT}s (attempt #${attempt})..."
sleep ${MAX_WAIT}
done
(( DEBUG )) && EchoD "Token is '${BLOG_ACCESS_TOKEN}'"
return 0
}
#==============================================================================
# ValidateTmdbKey: Check if the configured TMDb API key is valid
#------------------------------------------------------------------------------
# Description:
# Sends a test request to the TMDb API to validate the API key configured
# in the global variable `TMDB_API_KEY`. The request uses a known IMDb ID
# (`tt0030062`) to perform a simple lookup.
#
# If the response contains a `.status_code` field, this typically indicates
# an error such as invalid API key or unauthorized access. In that case,
# a warning is shown and `TMDB_KEY_VALID` is set to 0. Otherwise, the key
# is assumed to be valid, and `TMDB_KEY_VALID` is set to 1.
#
# Globals Used:
# TMDB_API_KEY - The TMDb API key to be tested
# DEBUG - If set, logs confirmation of validity
#
# Globals Set:
# TMDB_KEY_VALID - 1 if key is valid, 0 if invalid
#
# Outputs:
# - Echoes a warning if the API key appears to be invalid
# - Logs validation status if DEBUG is enabled
#
# Return:
# 0 - Always
#==============================================================================
ValidateTmdbKey() {
local response
response=$(curl --silent --get "https://api.themoviedb.org/3/find/tt0030062" \
--data-urlencode "api_key=${TMDB_API_KEY}" \
--data-urlencode "external_source=imdb_id")
if jq -e '.status_code' <<<"${response}" &>/dev/null; then
Echo "TMDb API key appears invalid. Actor data will be skipped unless --force-actors is used."
TMDB_KEY_VALID=0
else
(( DEBUG )) && EchoD "TMDb API key is valid."
TMDB_KEY_VALID=1
fi
}
#==============================================================================
# ShowVersion
#
# Display the script version information.
#
# Globals used:
# - SCRIPT_VERSION
# Globals modified:
# - None
#
# Arguments:
# None
#
# Return:
# 0 - Always
#==============================================================================
ShowVersion() {
echo "${0##*/} version ${SCRIPT_VERSION}"
}
#==============================================================================
# ShowHelp: Display detailed usage help for this script
#------------------------------------------------------------------------------
# Globals:
# SCRIPT_VERSION - Script version (used in --version output)
# Outputs:
# Prints all supported options, modes, and examples to stdout
#==============================================================================
ShowHelp() {
cat <<EOF
Usage: ${0##*/} [OPTIONS]
Modes:
--actor NAME Generate a filmography post for the specified actor
--imdb ID Use IMDb ID directly instead of actor name (e.g., nm0000493)
--movie-list Generate an update post based on modified movie folders
Actor Cache Utility:
--show-blogger-url Print Blogger URL for actor in cache
--delete-blogger-url Delete Blogger URL for actor in cache
--update-blogger-url URL Update Blogger URL for actor in cache
--revert-blogger-url [N] Revert Blogger URL to previous state (default N=1)
--history-blogger-url Show full Blogger URL change history
--show-photo-url Print photo URL for actor in cache
--delete-photo-url Delete photo URL for actor in cache
--update-photo-url URL Update photo URL for actor in cache
--revert-photo-url [N] Revert photo URL to previous state (default N=1)
--history-photo-url Show full photo URL change history
Dependency Check Modes (mutually exclusive):
--check-deps Check for required tools; exit if any are missing
--check-deps-local Check and fallback to local functions when possible
--check-deps-install Attempt to install missing tools via Cygwin installer
General Options:
-h, --help Show this help message and exit
-V, --version Show script version and exit
-v, --verbose Increase verbosity (can be repeated)
-D, --debug Enable debug output
-n, --dry-run Show what would happen, but make no changes
--force Override checks (e.g., update identical Blogger URL)
Movie Update Options (used with --movie-list):
-d, --date DATE Specify reference date (format: YYYY-MM-DD)
-y, --year YEAR Process only the given year
-r, --range RANGE Process a year range (e.g., 1930-1939 or -2020 or 1990-)
-t, --title TITLE Override default Blogger post title
-l, --label LABEL Add a Blogger label (can be repeated)
--publish Submit post as LIVE (default is DRAFT)
--force Skip confirmation warning when using --publish with year filters
--no-html Do not generate HTML in output
--force-actors Try to resolve actor info from TMDb even if key is invalid
Examples:
${0##*/} --actor "Jack Lemmon"
${0##*/} --imdb nm0000493
${0##*/} --actor "Jack Lemmon" --update-blogger-url 'https://...'
${0##*/} --actor "Jack Lemmon" --revert-url 2
${0##*/} --movie-list --date 2025-04-16 --range 1930-1939 --publish
${0##*/} --check-deps-local
EOF
}
#==============================================================================
# GetActorLifespan: Builds string "Name (YYYY–YYYY)" using parsed metadata
#------------------------------------------------------------------------------
# Description:
# Constructs a human-readable actor lifespan string using global metadata
# variables populated during actor data processing. The format follows:
#
# Name (YYYY–YYYY) ← if death year is known
# Name (YYYY– ) ← if actor is still living or date is unknown
#
# If either the birth or death year is not available, fallback values are
# used ("????" for birth, empty string for death). This string is primarily
# used in blog post titles and summaries.
#
# Globals Used:
# ACTOR_NAME - Actor's full name
# ACTOR_DOB - Actor's birth date (YYYY-MM-DD or partial)
# ACTOR_DOD - Actor's date of death (optional)
# DEBUG - If set, prints diagnostics
#
# Outputs:
# Echoes the formatted lifespan string to stdout
#
# Return:
# 0 - Always
#==============================================================================
GetActorLifespan() {
EchoD "Inside GetActorLifespan: ACTOR_NAME=${ACTOR_NAME}, ACTOR_DOB=${ACTOR_DOB}, ACTOR_DOD=${ACTOR_DOD}"
if [[ -z "${ACTOR_NAME}" ]]; then
EchoW "GetActorLifespan called before ACTOR_NAME was populated!"
fi
local name="${ACTOR_NAME}"
local birth="${ACTOR_DOB}"
local death="${ACTOR_DOD}"
# Format year of birth and death
local born="${birth:0:4}"
local died="${death:0:4}"
# Fallback logic
[[ -z "${born}" || "${born}" == "null" ]] && born="????"
[[ -z "${died}" || "${died}" == "null" ]] && died=""
# Compose final string
if [[ -n "${died}" ]]; then
echo "${name} (${born}–${died})"
else
echo "${name} (${born}– )"
fi
}
#==============================================================================
# GenerateActorFilmography
#
# Generate a full filmography HTML block for an actor, using only local cache
# (actor.db, movie.db, movie_credits.db, links.db). No unnecessary online fetch.
#
# Arguments:
# ${1} - Actor ID (either TMDb or IMDb ID)
#
# Globals Used:
# - ACTOR_DB_PATH
# - MOVIE_DB_PATH
# - MOVIE_CREDITS_DB_PATH
# - LINKS_DB_PATH
# - TMDB_API_KEY
# - DEBUG, VERBOSE
#
# Globals Set:
# - ACTOR_NAME, ACTOR_DOB, ACTOR_DOD, ACTOR_PHOTO_URL
# - ACTOR_TMDB_ID, ACTOR_IMDB_ID, ACTOR_BLOG_URL
# - ACTOR_FILMOGRAPHY (HTML output)
#
# Return:
# 0 - Success
# 1 - Failure
#==============================================================================
GenerateActorFilmography() {
local -r input_id="${1:-}"
local record=""
local movie_records=""
local html=""
local birth_year=""
local -i age=0
if [[ -z "${input_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Missing actor ID input."
return 1
fi
# Load actor metadata from local cache
if ! record=$(DBMovie_LoadActorRecord "${input_id}"); then
EchoE "${FUNCNAME[0]}:${LINENO}: Actor ID '${input_id}' not found in cache."
return 1
fi
IFS='|' read -r ACTOR_TMDB_ID ACTOR_IMDB_ID ACTOR_NAME ACTOR_DOB ACTOR_DOD ACTOR_PHOTO_URL ACTOR_BLOG_URL <<< "${record}"
ACTOR_BIRTH_YEAR="${ACTOR_DOB:0:4}"
EchoD "${FUNCNAME[0]}:${LINENO}: Loaded actor: ${ACTOR_NAME} (TMDb: ${ACTOR_TMDB_ID}, IMDb: ${ACTOR_IMDB_ID})"
# Load associated movie records
if ! movie_records=$(DBMovie_LoadMoviesByActor "${ACTOR_TMDB_ID}"); then
EchoE "${FUNCNAME[0]}:${LINENO}: No movie records found for actor TMDb ID ${ACTOR_TMDB_ID}."
return 1
fi
html+='<h2>Filmography: '"${ACTOR_NAME}"'</h2>'
html+='<table border="1" style="font-size: small;">'
html+='<tr style="background-color:#004080;color:white;font-weight:bold;">'
html+='<th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th></tr>'
while IFS='|' read -r tmdb_id imdb_id country title_en title_orig release_date watched_date blogger_url photo_url; do
[[ -z "${tmdb_id}" || "${tmdb_id}" == "TMDB_ID" ]] && continue # skip header or empty lines
local role=""
if ! role=$(DBMovie_GetMovieRole "${tmdb_id}" "${ACTOR_TMDB_ID}"); then
role=""
fi
local safe_role="$(echo "${role}" | sed -E 's/&/&/g; s/</\</g; s/>/\>/g')"
local release_year="${release_date:0:4}"
local imdb_link="${title_en}"
# Link to IMDb if available
if [[ -n "${imdb_id}" && "${imdb_id}" != "null" ]]; then
imdb_link="<a href=\"https://www.imdb.com/title/${imdb_id}\" target=\"_blank\">${title_en}</a>"
fi
# Age calculation
age="?"
if [[ -n "${ACTOR_BIRTH_YEAR}" && "${release_year}" =~ ^[0-9]{4}$ ]]; then
age=$(( release_year - ACTOR_BIRTH_YEAR ))
if (( age < 0 || age > 110 )); then
age="?"
fi
fi
local watch_mark=""
[[ -n "${watched_date}" ]] && watch_mark="✅"
(( VERBOSE > 1 )) && Echo " Movie: '${title_en}' Role: '${role}' Year: '${release_year}' Age: '${age}'"
html+="<tr><td>${release_date}</td><td>${imdb_link}</td><td>${safe_role}</td><td>${age}</td><td>${watch_mark}</td></tr>"
done <<< "${movie_records}"
html+="</table>"
export ACTOR_FILMOGRAPHY="${html}"
return 0
}
#==============================================================================
# ResolveActorImdbId: Resolves actor name to IMDb ID via TMDb
#------------------------------------------------------------------------------
# Description:
# This function performs a two-step resolution:
#
# 1. Uses the actor name to retrieve the corresponding TMDb person ID
# (via `GetActorTmdbId`, which queries TMDb).
# 2. Uses that TMDb ID to fetch external identifiers and extract the
# associated IMDb ID using the /external_ids endpoint.
#
# If any step fails, an error is printed and the function returns 1.
#
# Arguments:
# ${1} - Actor name (e.g., "Jack Lemmon")
#
# Globals Used:
# TMDB_API_KEY - The TMDb API key required for both queries
#
# Outputs:
# - Echoes the resolved IMDb ID (e.g., nm0000493) to stdout
# - Prints error messages via EchoE on failure
#
# Return:
# 0 - IMDb ID was successfully resolved
# 1 - Any error during TMDb resolution or API failure
#==============================================================================
ResolveActorImdbId() {
EchoD "${FUNCNAME[0]}:${LINENO}: $*"
local -r actor_name="${1}"
GetActorTmdbId "${actor_name}" || {
EchoE "${FUNCNAME[0]}:${LINENO}: Could not find TMDb ID for: ${actor_name}"
exit 1
}
local tmdb_id="${ACTOR_TMDB_ID_RESULT}"
if [[ "${tmdb_id}" == "${MANY_RECORD_MARK}" ]]; then
local -r result_json="${_ACTOR_SEARCH_RESULT_JSON}"
local -i index=0
Echo "Multiple matches found for '${actor_name}'. Loading additional information."
Echo
# Print header
Echo "Idx TMDB_ID Gender Name Depto Birth Death" >&${CALL_STDOUT_FD}
Echo "--- ------- ------- ---------------------- -------------- ---------- ----------" >&${CALL_STDOUT_FD}
local -i max_options=-1
local -a asOptions
while IFS='|' read -r idx id gender name dept; do
(( max_options++ ))
# Fetch additional metadata: birthday and deathday
local person_json=$(curl --silent --fail "https://api.themoviedb.org/3/person/${id}?api_key=${TMDB_API_KEY}")
asOptions[${max_options}]="$(printf "%-4s %-8s %-8s %-23s %-15s %-11s %-10s\n" \
"${idx}" \
"${id}" \
"${gender}" \
"${name}" \
"${dept}" \
$(jq -r '.birthday // "N/A"' <<< "${person_json}") \
$(jq -r '.deathday // "N/A"' <<< "${person_json}") )"
Echo "${asOptions[${max_options}]}"
done < <(jq -r '
.results
| to_entries[]
| "\(.key)|\(.value.id)|\(
if .value.gender == 1 then "Female"
elif .value.gender == 2 then "Male"
else "Unknown"
end)|\(.value.name)|\(.value.known_for_department)"
' <<< "${result_json}"
)
Echo
tmdb_id=
while true; do
EchoN "Select the correct TMDb from 0 to ${max_options} or 'q' to quit: "
read -u ${CALL_STDIN_FD} -r selection
[[ ${selection} -ge 0 && ${selection} -le ${max_options} ]] && {
tmdb_id="$(echo "${asOptions[${selection}]}"|awk '{print $2}')"; break;
}
selection="${selection:0:1}"
[[ "${selection,,}" == 'q' ]] && { Echo "Aborted by user."; exit 1 ; }
done
Echo "Using TMDB id ${tmdb_id}"
elif [[ -z "${tmdb_id}" ]]; then
EchoE "${FUNCNAME[0]}:${LINENO}: Could not resolve TMDb ID for: ${actor_name}"
exit 1
fi
# local -r imdb_id=$(curl --silent --fail "https://api.themoviedb.org/3/person/${tmdb_id}/external_ids?api_key=${TMDB_API_KEY}" | jq -r '.imdb_id // empty')
# [[ -z "${imdb_id}" ]] && {
# EchoE "Failed to resolve IMDb ID from TMDb ID ${tmdb_id}."
# exit 1
# }
DBMovie_LoadActorRecordParse "${tmdb_id}" >/dev/null || {
EchoE "${FUNCNAME[0]}:${LINENO}: Could not fetch actor metadata for IMDb ID: ${tmdb_id}"
return 1
}
echo "${ACTOR_IMDB_ID}"
return 0
}
#==============================================================================
# GetTopActorsFromTmdb: Generate collapsible HTML block with top 5 actors
#------------------------------------------------------------------------------
# Description:
# Given an IMDb movie ID and its release year, this function:
#
# 1. Resolves the corresponding TMDb movie ID.
# 2. Fetches the top cast members using the /credits endpoint.
# 3. For each of the top 5 cast members:
# - Fetches actor metadata (birth year)
# - Calculates their age at the time of release
# - Builds a row in an HTML <table> with actor name, role, and age
#
# The output is a collapsible <details> HTML section suitable for Blogger.
# It includes clickable TMDb profile links for each actor.
#
# Arguments:
# ${1} - IMDb ID of the movie (e.g., tt0030062)
# ${2} - Year of movie release (used for age calculation)
#
# Globals Used:
# TMDB_API_KEY - API key for TMDb
# VERBOSE - If >1, prints actor details to the screen for debugging
# DEBUG - Enables debug output for resolution failures
#
# Outputs:
# Echoes an HTML string block (<details> + <table>) to stdout
#
# Return:
# 0 - Success (even if partial or limited results)
# 1 - TMDb ID resolution failure or missing actor data
#==============================================================================
GetTopActorsFromTmdb() {
local -r imdb_id="${1}"
local -r release_year="${2}"
local tmdb_id
tmdb_id=$(curl --silent "https://api.themoviedb.org/3/find/${imdb_id}?api_key=${TMDB_API_KEY}&external_source=imdb_id" | jq -r '.movie_results[0].id')
if [[ -z "${tmdb_id}" || "${tmdb_id}" == "null" ]]; then
(( DEBUG )) && Echo "${FUNCNAME[0]}:${LINENO}: No TMDb ID found for IMDb: ${imdb_id}"
return
fi
local cast_json
cast_json=$(curl --silent "https://api.themoviedb.org/3/movie/${tmdb_id}/credits?api_key=${TMDB_API_KEY}")
local html='<details style="margin-top:8px;"><summary style="cursor:pointer;font-weight:bold;">[+] Top Cast</summary>'
html+='<table border="1" style="font-size: small; margin-top:5px;"><tr><th>Actor</th><th>Role</th><th>Age</th></tr>'
for i in {0..5}; do
local actor_id name role birthdate age
actor_id=$(jq -r ".cast[${i}].id" <<<"${cast_json}")
name=$(jq -r ".cast[${i}].name" <<<"${cast_json}")
role=$(jq -r ".cast[${i}].character" <<<"${cast_json}")
[[ -z "${actor_id}" || "${actor_id}" == "null" ]] && continue
birthdate=$(curl --silent "https://api.themoviedb.org/3/person/${actor_id}?api_key=${TMDB_API_KEY}" | jq -r '.birthday')
if [[ "${birthdate}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
local birth_year="${birthdate:0:4}"
age=$(( year - birth_year ))
else
age="?"
fi
# Build HTML with hyperlink
local actor_link="<a href=\"https://www.themoviedb.org/person/${actor_id}\" target=\"_blank\">${name}</a>"
(( VERBOSE > 1 )) && Echo " Actor: '${name}' Role: '${role}' Age: '${age}'"
html+="<tr><td>${actor_link}</td><td>${role}</td><td>${age}</td></tr>"
done
html+='</table></details>'
echo "${html}"
}
#==============================================================================
# GetTitleFromOmdb: Resolves proper English title and metadata using OMDb API
#------------------------------------------------------------------------------
# Description:
# Queries the OMDb API to fetch accurate metadata for a movie given its title
# and expected release year. Results are cached in-memory using a global
# associative array (METADATA_CACHE) to avoid redundant lookups.
#
# Fields retrieved include:
# - Proper English title
# - Genre(s)
# - IMDb ID
# - Runtime
# - Poster URL
#
# If the API response year does not match the expected year, a warning is
# issued and a fallback record is returned with the raw query string and "N/A"
# placeholders.
#
# Arguments:
# ${1} - Search query (movie title, as string)
# ${2} - Expected release year (YYYY)
#
# Globals Used:
# OMDB_API_KEY - OMDb API key
# DEBUG - Enables debug output
# METADATA_CACHE[] - Associative array for in-memory caching
#
# Outputs:
# Echoes pipe-separated result string:
# Title|||Genre|||IMDbID|||Runtime|||Poster
#
# Return:
# 0 - Success (valid result or fallback returned)
# 1 - None (all control handled internally; no external exit code)
#==============================================================================
GetTitleFromOmdb() {
local -r query="${1}"
local -r expected_year="${2}"
local -r api_url="http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&t=$(urlencode "${query}")&y=${expected_year}&type=movie"
# Check if already cached
local cache_key="${query}_${expected_year}"
if [[ -n "${METADATA_CACHE["${cache_key}"]}" ]]; then
echo "${METADATA_CACHE["${cache_key}"]}"
return
fi
local json
json=$(curl --silent --max-time 10 "${api_url}")
local success
success=$(jq -r '.Response' <<< "${json}")
local actual_year
actual_year=$(jq -r '.Year // empty' <<< "${json}")
# Check for success and valid year
if [[ "${success}" == "True" && "${actual_year}" == "${expected_year}" ]]; then
local proper_title=$(jq -r '.Title // empty' <<< "${json}")
local genre=$(jq -r '.Genre // empty' <<< "${json}")
local imdbid=$(jq -r '.imdbID // empty' <<< "${json}")
local runtime=$(jq -r '.Runtime // empty' <<< "${json}")
local poster=$(jq -r '.Poster // empty' <<< "${json}")
local result="${proper_title}|||${genre}|||${imdbid}|||${runtime}|||${poster}"
METADATA_CACHE["${cache_key}"]="${result}"
(( DEBUG )) && EchoD "✅ OMDb title resolved: ${proper_title} [${genre}]"
echo "${result}"
else
EchoW "OMDb lookup mismatch or failure: Wanted ${expected_year}, got '${actual_year}' for '${query}'"
local fallback="${query}|||N/A|||N/A|||N/A|||N/A"
METADATA_CACHE["${cache_key}"]="${fallback}"
echo "${fallback}"
fi
}
#==============================================================================
# GetActorPhotoUrl: Returns TMDb profile image URL for an actor
#------------------------------------------------------------------------------
# Description:
# Given an IMDb actor ID stored in ACTOR_IMDB_ID, this function queries TMDb's
# "find" endpoint to retrieve the actor's profile image path. If a profile image
# exists, it returns the full TMDb URL to a 300px-wide version of the image.
#
# Globals Used:
# ACTOR_IMDB_ID - IMDb ID of the actor (e.g., nm0000493)
# TMDB_API_KEY - TMDb API key for querying person metadata
#
# Outputs:
# Echoes the full profile image URL (https://image.tmdb.org/...) or empty string
#
# Return:
# 0 - Success (URL echoed or empty string)
# 1 - Function will not exit with error; result is determined by output
#==============================================================================
GetActorPhotoUrl() {
local -r tmdb_find_url="https://api.themoviedb.org/3/find/${ACTOR_IMDB_ID}?api_key=${TMDB_API_KEY}&external_source=imdb_id"
local profile_path
profile_path=$(curl --silent --fail "${tmdb_find_url}" | jq -r '.person_results[0].profile_path')
if [[ -n "${profile_path}" && "${profile_path}" != "null" ]]; then
echo "https://image.tmdb.org/t/p/w300${profile_path}"
else
echo ""
fi
}
#==============================================================================
# SubmitBlogDraft: Submits or updates a Blogger DRAFT post with HTML content
#------------------------------------------------------------------------------
# Description:
# Sends a draft post to the Blogger API using OAuth authentication.
# If a path is provided, attempts to update an existing post; otherwise,
# creates a new draft. In DRYRUN mode, outputs intended payload without posting.
#
# Globals:
# BLOG_ACCESS_TOKEN - OAuth 2.0 Bearer token for Blogger API
# BLOG_ID - Blogger blog ID to submit the post to
# CUSTOM_TITLE - Title of the blog post (used if not overridden)
# DRYRUN - If set to 1, skips API call and shows simulated output
# DEBUG - If set, emits debug logs using EchoD
#
# Arguments:
# ${1} - HTML body content (unescaped raw HTML)
# ${2} - Optional post path (e.g., "2025/04/some-title.html") to update
#
# Outputs:
# On success, echoes the full Blogger post URL (https://...)
# On failure, prints error message and returns non-zero exit status
#==============================================================================
SubmitBlogDraft() {
local -r html_content="${1}"
local -r post_path="${2:-}"
local -r blog_title="${CUSTOM_TITLE:-Actor - ${ACTOR_QUERY} Filmography}"
(( DEBUG )) && EchoD "${FUNCNAME[0]}| Title=${blog_title}"
(( DEBUG )) && EchoD "${FUNCNAME[0]}| Update=${post_path:-<new>}"
if (( DRYRUN )); then
Echo "[DRY-RUN] Blog post would be submitted with title: ${blog_title}"
Echo "[DRY-RUN] HTML Content:"
Echo "${html_content}"
return 0
fi
local -r payload="$(
jq -n \
--arg title "${blog_title}" \
--arg content "${html_content}" \
--arg blog_id "${BLOG_ID}" \
'{
kind: "blogger#post",
blog: { id: $blog_id },
title: $title,
content: $content
}'
)"
local response
local url
GetBlogAccessToken || {
EchoE "Failed to retrieve Blogger API access token."
return 1
}
if [[ -n "${post_path}" ]]; then
response="$(curl --silent --fail --show-error --request PUT \
--header "Authorization: Bearer ${BLOG_ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--data "${payload}" \
"https://www.googleapis.com/blogger/v3/blogs/${BLOG_ID}/posts/bypath?path=${post_path}")"
url="$(jq -r '.url // empty' <<< "${response}")"
if [[ -z "${url}" ]]; then
EchoE "Blogger update failed. Response: ${response}"
return 1
fi
echo "${url}"
else
response="$(curl --silent --fail --show-error --request POST \
--header "Authorization: Bearer ${BLOG_ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--data "${payload}" \
"https://www.googleapis.com/blogger/v3/blogs/${BLOG_ID}/posts/")"
url="$(jq -r '.url // empty' <<< "${response}")"
if [[ -z "${url}" ]]; then
EchoE "Blogger submission failed. Response: ${response}"
return 1
fi
echo "${url}"
fi
}
#==============================================================================
# BuildHtmlTable: Constructs the grouped HTML table from collected movie data
#------------------------------------------------------------------------------
# Description:
# Uses the associative array `year_map` (year → HTML row list) to build a
# full HTML <table> with one row per year and a bullet list of updated or
# added movie titles. Sorting is done alphabetically within each year using
# ASCII-safe, case-insensitive sorting (LC_ALL=C).
#
# Globals Used:
# REFERENCE_DATE - Date string used in header intro
# year_map - Associative array: [year] → list of raw HTML lines
#
# Outputs:
# Echoes full HTML block to stdout
#
# Return:
# 0 - Success
#==============================================================================
BuildHtmlTable() {
local -r intro="These are the movie titles added (new titles) or updated (new format for an existing title) on ${REFERENCE_DATE}:<br><br>"
local html='<!-- Generated by MovieUpdateDraft.sh -->'
html+='<table border="1">'
html+='<tr style="background-color:#004080;color:white;font-weight:bold;"><th>Year</th><th>Updated Titles</th></tr>'
for year in $(printf "%s\n" "${!year_map[@]}" | sort); do
local raw_entries
IFS=$'\n' read -rd '' -a raw_entries < <(printf "%s\n" "${year_map[$year]}" | grep -v '^\s*$' && printf '\0')
# Build sortable pairs: TITLE|||HTML_LINE
local -a sortable_lines=()
for line in "${raw_entries[@]}"; do
line=$(echo "${line}" | tr -d '\r\n')
# Try to extract title from anchor tag
local display_text
if [[ "${line}" =~ $'^<a [^>]*>([^<]+)</a>' ]]; then
display_text="${BASH_REMATCH[1]}"
else
# Fallback: use first few words from line
display_text=$(echo "${line}" | sed -E 's/^([^<]+) .*/\1/')
fi
# Remove any multibyte/UTF-8 characters just for sorting
display_text=$(echo "${display_text}" | iconv -c -f UTF-8 -t ASCII//TRANSLIT)
sortable_lines+=( "${display_text}|||${line}" )
done
# Sort by display title (case-insensitive, ASCII-safe)
local -a sorted_lines=()
IFS=$'\n' read -rd '' -a sorted_lines < <(
printf "%s\n" "${sortable_lines[@]}" | LC_ALL=C sort -f | sed 's/^[^|]*|||//' && printf '\0'
)
html+="<tr><td>${year}</td><td><ul>"
for sorted_line in "${sorted_lines[@]}"; do
html+="<li>${sorted_line}</li>"
done
html+="</ul></td></tr>"
done
html+="</table>"
printf "%s" "${intro}${html}"
}
#==============================================================================
# BuildSummaryHtml: Generate a visual summary block for updated movie entries
#------------------------------------------------------------------------------
# Description:
# Constructs an HTML-formatted summary of changes made for the reference
# date. It includes total counts (years, titles, added/updated) and a
# breakdown by year. Uses emojis and bold formatting for clarity.
#
# Globals Used:
# year_map - Associative array of year → titles
# year_added_count - Associative array of year → count of added titles
# year_changed_count - Associative array of year → count of updated titles
# total_added - Total number of newly added titles
# total_changed - Total number of updated titles
#
# Outputs:
# Echoes a full HTML <ul>-formatted block with summary data to stdout
#
# Return:
# 0 - Success
#==============================================================================
BuildSummaryHtml() {
local -i total_years=${#year_map[@]}
local -i total_titles=$((total_added + total_changed))
local html='<br><br><b>Summary:</b><br>'
html+='<ul>'
html+="<li>🗓️ Total Years Updated: <b>${total_years}</b></li>"
html+="<li>🎬 Total Titles: <b>${total_titles}</b></li>"
html+="<li>🆕 New Titles: <b>${total_added}</b></li>"
html+="<li>♻️ Updated Titles: <b>${total_changed}</b></li>"
html+='</ul>'
html+='<b>Details per Year:</b><br>'
html+='<ul>'
for year in $(printf "%s\n" "${!year_map[@]}" | sort); do
local -i added=${year_added_count["${year}"]:-0}
local -i changed=${year_changed_count["${year}"]:-0}
html+="<li><b>${year}</b>: 🆕 ${added}, ♻️ ${changed}</li>"
done
html+='</ul>'
printf "%s" "${html}"
}
#==============================================================================
# ScanDirectories: Traverse movie directories and detect updates by date
#------------------------------------------------------------------------------
# Description:
# Walks through all year-based subdirectories under ${MOVIE_ROOT}, identifying
# directories that have been modified or added based on file timestamps
# between REFERENCE_DATE_EPOCH and TODAY_EPOCH. For each qualifying folder:
# - Determines if it is a new title or an update
# - Retrieves OMDb metadata (title, genre, runtime, IMDb ID)
# - Optionally fetches top actors from TMDb (if enabled)
# - Formats HTML blocks with poster, cast, and metadata
#
# Updates year_map[], year_added_count[], year_changed_count[],
# total_added and total_changed globals accordingly.
#
# Globals Used:
# MOVIE_ROOT - Root path to year-based movie folders
# REFERENCE_DATE - Target date in YYYY-MM-DD format
# REFERENCE_DATE_EPOCH - Epoch of reference date
# TODAY_EPOCH - Epoch of today’s date
# VERBOSE - Controls verbosity of output
# DEBUG - Enables debug logging
# FILTER_YEARS[] - Optional filter list of years to include
# FORCE_ACTORS - If set, actor info is always fetched
# TMDB_KEY_VALID - TMDb key validation result
# year_map[] - Associative array for HTML generation
# year_added_count[] - Count of added titles per year
# year_changed_count[] - Count of updated titles per year
# total_added - Total count of new titles
# total_changed - Total count of changed titles
#
# Outputs:
# Populates year_map with resolved HTML snippets per year
# Updates count globals for summary reporting
#
# Return:
# 0 - Always
#==============================================================================
ScanDirectories() {
Echo "Scanning directories for updates on ${REFERENCE_DATE}..."
# Stats tracking
declare -gA year_added_count=()
declare -gA year_changed_count=()
declare -gi total_added=0
declare -gi total_changed=0
for year_dir in "${MOVIE_ROOT}"/*; do
[[ -d "${year_dir}" ]] || continue
[[ "${year_dir##*/}" =~ ^! ]] && continue
local year="${year_dir##*/}"
[[ "${year}" =~ ^[0-9]{4}$ ]] || continue
# Apply year filter if provided
if [[ ${#FILTER_YEARS[@]} -gt 0 ]]; then
local -i match=0
for filter in "${FILTER_YEARS[@]}"; do
[[ "${year}" == "${filter}" ]] && match=1 && break
done
(( match == 0 )) && continue
fi
Echo "Processing year: ${year}"
while IFS= read -r -d '' title_dir; do
[[ "${title_dir##*/}" =~ ^! ]] && continue
local folder_name="${title_dir##*/}"
(( DEBUG )) && [[ -z "${folder_name}" ]] && EchoD "⚠️ Empty folder name extracted from: ${title_dir}"
[[ -z "${folder_name}" ]] && continue
(( DEBUG )) && EchoD " Checking title: ${folder_name}"
local -i matched=0
local -i is_added=1
while IFS= read -r -d '' file; do
local target
target=$(readlink -f "${file}")
[[ -z "${target}" ]] && continue
local -i file_epoch
file_epoch=$(stat -c %Y "${target}")
(( DEBUG )) && EchoD " File: ${file} -> Target: ${target} (Epoch: ${file_epoch})"
if (( file_epoch >= REFERENCE_DATE_EPOCH && file_epoch < TODAY_EPOCH )); then
[[ -e "${title_dir}/metadata.json" ]] && is_added=0
local update_type
update_type=$([[ ${is_added} -eq 1 ]] && echo "Added" || echo "Changed")
# Count BEFORE OMDb (even if OMDb fails)
if (( is_added )); then
(( year_added_count["${year}"]++ ))
(( total_added++ ))
else
(( year_changed_count["${year}"]++ ))
(( total_changed++ ))
fi
# OMDb call
local resolved_full
resolved_full=$(GetTitleFromOmdb "${folder_name}" "${year}")
local resolved_title="${resolved_full%%|||*}"
resolved_title="$(echo "${resolved_title}" | xargs)" # <-- Trim after assigning
EchoD "resolved_title='${resolved_title}' from resolved_full='${resolved_full}'"
if [[ -z "${resolved_title}" || "${resolved_title}" == "null" ]]; then
resolved_title="${folder_name}"
(( DEBUG )) && EchoD "Fallback: using folder name as title: '${resolved_title}'"
fi
# Verbose output of matched title
if (( VERBOSE == 1 )); then
Echo " ${resolved_title} (updated ${REFERENCE_DATE})"
elif (( VERBOSE > 1 )); then
Echo " -> MATCHED: ${resolved_title} (updated ${REFERENCE_DATE})"
fi
local temp_rest="${resolved_full#*|||}"
local resolved_genre="${temp_rest%%|||*}"
temp_rest="${temp_rest#*|||}"
local resolved_imdbid="${temp_rest%%|||*}"
temp_rest="${temp_rest#*|||}"
local resolved_runtime="${temp_rest%%|||*}"
local resolved_poster="${temp_rest#*|||}"
(( DEBUG )) && EchoD "Calling TMDb with IMDb ID: ${resolved_imdbid}"
if (( FORCE_ACTORS )) || (( TMDB_KEY_VALID )); then
cast_html="$(GetTopActorsFromTmdb "${resolved_imdbid}" "${year}")"
#EchoD "='${cast_html}'"
fi
local poster_html=""
# Start table if either poster or cast exists
if [[ -n "${resolved_poster}" && "${resolved_poster}" != "N/A" || -n "${cast_html}" ]]; then
poster_html='<table><tr>'
# Poster column
if [[ -n "${resolved_poster}" && "${resolved_poster}" != "N/A" ]]; then
poster_html+='<td style="vertical-align:top;">'
poster_html+='<img src="'"${resolved_poster}"'" width="120">'
poster_html+='</td>'
fi
# Cast column
if [[ -n "${cast_html}" ]]; then
poster_html+='<td style="padding-left:8px; vertical-align:top;">'
poster_html+="${cast_html}"
poster_html+='</td>'
fi
poster_html+='</tr></table>'
fi
# Formatting
local genre_suffix=""
if [[ -n "${resolved_genre}" && "${resolved_genre}" != "N/A" ]]; then
genre_suffix=" <span style=\"color:#800080;font-weight:bold;\">[${resolved_genre}, ${resolved_runtime}]</span>"
fi
local linked_title="${resolved_title}"
if [[ -n "${resolved_imdbid}" && "${resolved_imdbid}" != "N/A" ]]; then
linked_title="<a href=\"https://www.imdb.com/title/${resolved_imdbid}\" target=\"_blank\">${resolved_title}</a>"
fi
(( DEBUG )) && EchoD "✔ Counted '${resolved_title}' as ${update_type} for year ${year}"
year_map["${year}"]+="${linked_title}${genre_suffix} (${update_type})${poster_html}"$'\n'
matched=1
break
fi
done < <(find "${title_dir}" -type l -print0)
(( matched == 0 && VERBOSE > 1 )) && Echo " -> No file matched."
done < <(find "${year_dir}" -mindepth 1 -maxdepth 1 -type d -print0)
done
}
#==============================================================================
# ParseArguments: Parse all command-line arguments and populate global variables.
#------------------------------------------------------------------------------
# Description:
# Parses CLI flags to determine execution mode (e.g., --actor or --movie-list),
# along with filters, toggles, and post behavior. Sets global control variables.
#
# Globals Set:
# EXECUTION_MODE, EXECUTION_SUBMODE, ACTOR_QUERY, IMDB_ID,
# DRYRUN, VERBOSE, DEBUG, FORCE, FORCE_ACTORS, FORCE_PUBLISH,
# CUSTOM_DATE, CUSTOM_TITLE, LABELS[], FILTER_YEARS[],
# CHECK_DEPS_DEFAULT, CHECK_DEPS_LOCAL, CHECK_DEPS_INSTALL,
# VERBOSE_DEPS, USE_HTML, UPDATE_MODE, NEW_BLOGGER_URL, REVERT_STEPS
#
# Outputs:
# - Populates globals based on flags
# - Echoes errors and exits on invalid combinations
#==============================================================================
ParseArguments() {
local -i nActorSwitchUsed=0
local -i nMovieListUsed=0
NEW_BLOGGER_URL=
while [[ $# -gt 0 ]]; do
case ${1} in
-h|--help) ShowHelp; exit 0 ;;
-V|--version) ShowVersion; exit 0 ;;
-n|--dry-run) DRYRUN=1 ; shift ;;
-v|--verbose) ((VERBOSE++)) ; shift ;;
-D|--debug) ((DEBUG++)) ; shift ;;
--check-deps) CHECK_DEPS_DEFAULT=1 ; shift ;;
--check-deps-local) CHECK_DEPS_LOCAL=1 ; shift ;;
--check-deps-install) CHECK_DEPS_INSTALL=1 ; shift ;;
#-------------------------------------- ACTOR MODE
--actor)
shift
if [[ -z "$1" ]]; then
EchoE "--actor requires a name (e.g., --actor 'Jack Lemmon')"
exit 1
fi
ACTOR_QUERY="$1"
EXECUTION_MODE=2
EXECUTION_SUBMODE=0
nActorSwitchUsed=1
shift
;;
--force) FORCE=1 ; shift ;;
--tmdb)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
shift
if [[ -z "$1" || ! "$1" =~ ^[0-9]{3,}$ ]]; then
EchoE "--tmdb requires a valid TMDb ID (e.g., 4392)"
exit 1
fi
ACTOR_TMDB_ID="$1"
shift
;;
--imdb)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
shift
if [[ -z "$1" || ! "$1" =~ ^nm[0-9]{7,}$ ]]; then
EchoE "--imdb requires a valid IMDb ID (e.g., nm0000493)"
exit 1
fi
ACTOR_IMDB_ID="$1"
shift
;;
--show-blogger-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
EXECUTION_SUBMODE=1
shift
;;
--delete-blogger-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
EXECUTION_SUBMODE=2
shift
;;
--update-blogger-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
shift
if [[ -z "$1" ]]; then
EchoE "--update-blogger-url requires a URL value"
exit 1
fi
EXECUTION_SUBMODE=3
NEW_BLOGGER_URL="$1"
(( DEBUG )) && EchoD "NEW_BLOGGER_URL='${NEW_BLOGGER_URL}'"
shift
;;
--revert-blogger-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
REVERT_STEPS="${2:-1}"
EXECUTION_SUBMODE=4
shift 2
;;
--history-blogger-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
EXECUTION_SUBMODE=5
shift
;;
--show-photo-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
EXECUTION_SUBMODE=6
shift
;;
--delete-photo-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
EXECUTION_SUBMODE=7
shift
;;
--update-photo-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
shift
if [[ -z "$1" ]]; then
EchoE "--update-photo-url requires a URL value"
exit 1
fi
EXECUTION_SUBMODE=8
NEW_PHOTO_URL="$1"
(( DEBUG )) && EchoD "NEW_PHOTO_URL='${NEW_PHOTO_URL}'"
shift
;;
--revert-photo-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
REVERT_STEPS="${2:-1}"
EXECUTION_SUBMODE=9
shift 2
;;
--history-photo-url)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
EXECUTION_SUBMODE=10
shift
;;
--force-bio)
(( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; }
FORCE_BIO=1
shift ;;
#-------------------------------------- MOVIE LIST MODE
--movie-list)
if (( EXECUTION_MODE != 0 )); then
EchoE "--movie-list cannot be used with another execution mode."
exit 1
fi
EXECUTION_MODE=1
nMovieListUsed=1
shift
;;
--update)
shift
UPDATE_MODE=1
if [[ -n "${1}" && "${1}" != --* ]]; then
ACTOR_UPDATE_PATH="${1}"
shift
fi
;;
-d|--date) CUSTOM_DATE="${2}" ; shift 2 ;;
--force-actors) FORCE_ACTORS=1 ; shift ;;
--no-html) USE_HTML=0 ; shift ;;
--publish) FORCE_PUBLISH=1 ; shift ;;
-t|--title) shift; CUSTOM_TITLE="$1"; shift ;;
-l|--label) shift; LABELS+=("$1"); shift ;;
-y|--year)
[[ "$2" =~ ^[0-9]{4}$ ]] || { EchoE "Invalid year format: $2" ; exit 1; }
FILTER_YEARS+=("$2")
shift 2
;;
-r|--range)
shift
if [[ "$1" =~ ^([0-9]{4})-([0-9]{4})$ ]]; then
start=${BASH_REMATCH[1]}
end=${BASH_REMATCH[2]}
elif [[ "$1" =~ ^([0-9]{4})-$ ]]; then
start=${BASH_REMATCH[1]}
end=$(( start + 9 ))
elif [[ "$1" =~ ^-([0-9]{4})$ ]]; then
end=${BASH_REMATCH[1]}
start=$(( end - (end % 10) ))
else
EchoE "Invalid range format: $1"
exit 1
fi
if (( start > end )); then
EchoE "Invalid range: ${start}-${end}"
exit 1
fi
for (( y=start; y<=end; y++ )); do
FILTER_YEARS+=("${y}")
done
shift
;;
--) shift ; break ;;
-*) EchoE "Unknown option: ${1}" ; PrintHelp ; exit 1 ;;
*) POSITIONAL_ARGS+=("${1}") ; shift ;;
esac
done
(( nMovieListUsed==1 && nActorSwitchUsed==1 )) && { EchoE "--movie-list cannot be combined with --actor"; exit 1; }
(( nMovieListUsed==0 && nActorSwitchUsed==0 )) && { EchoE "No valid mode specified. Use --movie-list or --actor"; exit 1; }
if (( nActorSwitchUsed==1 )); then
if [[ -n "${ACTOR_QUERY}" && -n "${IMDB_ID}" ]]; then
EchoW "Both --actor and --imdb specified. IMDb ID will take precedence."
ACTOR_QUERY=""
fi
fi
}
#==============================================================================
# DoDependency: Validate or install required tools with fallback and verbosity
#------------------------------------------------------------------------------
# Description:
# Checks for required and optional dependencies:
# - Required: curl, jq, date, urlencode
# - Optional (feature-specific): pup
#
# Mode of operation is controlled by:
# - CHECK_DEPENDENCIES: verify and report missing (no fallback/install)
# - CHECK_DEPS_LOCAL: check and fallback if possible (e.g., DefineUrlencodeFallback)
# - CHECK_DEPS_INSTALL: force install mode (assumes package manager is available)
# - VERBOSE_DEPS: if set, displays version info for available tools
#
# Globals Used:
# CHECK_DEPENDENCIES
# CHECK_DEPS_LOCAL
# CHECK_DEPS_INSTALL
# VERBOSE_DEPS
#
# Outputs:
# - Echo output for each tool, including version or status
# - Fallback logic invoked for 'urlencode' if missing and local mode enabled
# - Exits with code 1 if any required tool is missing and not handled
#==============================================================================
DoDependency() {
local -a required_tools=("curl" "jq" "date" "urlencode")
local -a optional_tools=("pup")
local -i missing=0
if (( CHECK_DEPS_INSTALL )); then
Echo "🔧 Forcing install of all required tools..."
for tool in "${required_tools[@]}"; do
EnsureDependency "${tool}" "${tool}"
done
for opt in "${optional_tools[@]}"; do
EnsureDependency "${opt}" "${opt}" "optional"
done
Echo "✅ All required tools have been installed or handled."
elif (( CHECK_DEPS_LOCAL )); then
Echo "🔍 Checking tools and applying local fallback logic (no install)..."
for tool in "${required_tools[@]}"; do
if command -v "${tool}" &>/dev/null; then
Echo "✔ '${tool}' available"
(( VERBOSE_DEPS )) && "${tool}" --version 2>&1 | head -n 1 | sed 's/^/ ↪ /'
else
case "${tool}" in
urlencode)
DefineUrlencodeFallback
Echo "ℹ️ Defined fallback for '${tool}'"
;;
*)
EchoE "❌ Required tool '${tool}' is missing and no fallback is available."
(( missing++ ))
;;
esac
fi
done
for opt in "${optional_tools[@]}"; do
if command -v "${opt}" &>/dev/null; then
Echo "✔ Optional tool '${opt}' available"
(( VERBOSE_DEPS )) && "${opt}" --version 2>&1 | head -n 1 | sed 's/^/ ↪ /'
else
EchoW "Optional tool '${opt}' is not available. Some features may be disabled."
fi
done
if (( missing > 0 )); then
EchoE "⛔ ${missing} required tool(s) could not be handled. Aborting."
exit 1
else
Echo "✅ All required tools are available or have been handled with fallback."
fi
elif (( CHECK_DEPENDENCIES )); then
Echo "🔍 Verifying dependencies only (no install, no fallback)..."
for tool in "${required_tools[@]}"; do
if command -v "${tool}" &>/dev/null; then
Echo "✔ '${tool}' available"
(( VERBOSE_DEPS )) && "${tool}" --version 2>&1 | head -n 1 | sed 's/^/ ↪ /'
else
EchoE "❌ Missing required tool: ${tool}"
(( missing++ ))
fi
done
for opt in "${optional_tools[@]}"; do
if command -v "${opt}" &>/dev/null; then
Echo "✔ Optional tool '${opt}' available"
(( VERBOSE_DEPS )) && "${opt}" --version 2>&1 | head -n 1 | sed 's/^/ ↪ /'
else
EchoW "⚠️ Optional tool '${opt}' is not available. Some features may be disabled."
fi
done
if (( missing > 0 )); then
EchoE "⛔ Missing ${missing} required tool(s). Use --check-deps-local or --check-deps-install."
exit 1
else
Echo "✅ All required tools are present."
fi
fi
}
#==============================================================================
# ParseImdbMiniBio: Extracts the Mini Bio HTML section from downloaded IMDb bio
#------------------------------------------------------------------------------
# Description:
# Parses a downloaded IMDb biography HTML file and extracts the "Mini Bio"
# section. It targets the block identified by 'data-testid="sub-section-mini_bio"'
# and cleans the HTML into plain text. The result is saved to the output file.
#
# Arguments:
# ${1} - Path to the input HTML file downloaded from IMDb
# ${2} - Path to the output text file where the parsed bio should be saved
#
# Globals:
# FUNCNAME - Used for logging/debug output
#
# Outputs:
# Writes plain-text biography content to the specified output file.
#
# Exit Codes:
# None explicitly. Errors may surface from sed or file access issues.
#==============================================================================
ParseImdbMiniBio() {
local -r input_file="${1}"
local -r output_file="${2}"
# Attempt to extract bio inside 'data-testid="sub-section-mini_bio"' block
sed -n '/data-testid="sub-section-mini_bio"/,/<\/section>/p' "${input_file}" |
sed -n '/<div class="ipc-html-content-inner-div"/,/<\/div><\/div><\/div>/p' |
sed 's/<[^>]*>//g; s/"/"/g; s/'/'\''/g; s/ / /g' |
sed '/^[[:space:]]*$/d' > "${output_file}"
}
#==============================================================================
# EnsureActorMiniBioCached: Ensures IMDb Mini Bio is downloaded and cached
#------------------------------------------------------------------------------
# Description:
# Downloads and caches the Mini Bio section from an actor's IMDb bio page.
# Creates <imdb_id>_bio.db under ${MY_MDB_CACHE}/ containing the extracted
# paragraph. Also appends a permalink to links.db if not already present.
#
# Globals:
# MY_MDB_CACHE - Path to the local metadata cache directory
# FUNCNAME - Used for debug/error messages
# FORCE_BIO - If set, redownload even if cached
# ACTOR_BIO_HTML - (Set) Parsed bio HTML loaded into memory
#==============================================================================
EnsureActorMiniBioCached() {
local -r imdb_id="${1}"
local -r mini_bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db"
local -r imdb_bio_url="https://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm"
local -r links_file="${MY_MDB_CACHE}/links.db"
ACTOR_BIO_HTML=
if (( ! FORCE_BIO )) && [[ -s "${mini_bio_file}" ]]; then
ACTOR_BIO_HTML="$(< "${mini_bio_file}")"
return 0
fi
local tmp_bio_html="$(mktemp)"
# Use spoofed User-Agent to bypass 403
#curl --silent --fail \
# --header "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" \
# "${imdb_bio_url}" > "${tmp_bio_html}"
wget --quiet \
--header="User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" \
--output-document="${tmp_bio_html}" \
"${imdb_bio_url}"
ParseImdbMiniBio "${tmp_bio_html}" "${mini_bio_file}"
rm -f "${tmp_bio_html}"
# Validate and set memory cache
if [[ ! -s "${mini_bio_file}" ]]; then
EchoW "${FUNCNAME[0]}:${LINENO}: IMDb Mini Bio could not be extracted: ${mini_bio_file}"
ACTOR_BIO_HTML="<p><em>Biography not available.</em></p>"
echo "${ACTOR_BIO_HTML}" > "${mini_bio_file}"
else
ACTOR_BIO_HTML="$(< "${mini_bio_file}")"
EchoD "${FUNCNAME[0]}:${LINENO}:✅ IMDb Mini Bio saved to ${mini_bio_file}"
fi
if ! grep -q "^${imdb_id}[[:space:]]+.*#mini_bio" "${LINKS_DB_PATH}" 2>/dev/null; then
echo -e "${imdb_id}|https://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm#mini_bio" >> "${LINKS_DB_PATH}"
EchoD "${FUNCNAME[0]}:${LINENO}:🔗 Added IMDb Mini Bio link to ${LINKS_DB_PATH}"
fi
}
#==============================================================================
# GenerateActorFilmographyFromTmdb - Build full actor filmography HTML from TMDb
#------------------------------------------------------------------------------
# Description:
# Retrieves filmography for an actor using TMDb API, falling back to local
# cache when available. The output includes photo, bio, movie table, and
# external links. Movies are fetched via movie_credits API and sorted by
# release date. Metadata is stored in ~/.MyMDBCache/*.db files.
#
# If DRYRUN is enabled, metadata will not be saved. Roles are sanitized for
# HTML, and a red ❌ is shown for films released after the actor’s death.
#
# Globals:
# ACTOR_IMDB_ID - IMDb ID of the actor (RO)
# ACTOR_QUERY - Actor name for fallback messaging (RO)
# ACTOR_NAME - Actor name (RW, exported here)
# ACTOR_DOB - Date of birth, YYYY-MM-DD (RW, exported here)
# ACTOR_DOD - Date of death, if any (RW, exported here)
# ACTOR_BIRTH_YEAR - Year of birth as integer (RW, exported here)
# ACTOR_PHOTO_URL - Full TMDb profile image URL (RW, exported here)
# ACTOR_BLOG_URL - Cached Blogger post URL (RW, exported here)
# ACTOR_TMDB_ID - TMDb actor ID (RW, set internally)
# ACTOR_FILMOGRAPHY - HTML output block (RW, set by this function)
# TMDB_API_KEY - TMDb API key (RO)
# MY_MDB_CACHE - Path to ~/.MyMDBCache (RO)
# LINKS_DB_PATH - Path to links.db (RO)
# DRYRUN - If non-zero, avoids writing to cache (RO)
# VERBOSE - If non-zero, enables verbose output (RO)
# DEBUG - If non-zero, enables debug logging (RO)
#
# Outputs:
# Sets ACTOR_FILMOGRAPHY with HTML content including:
# - Actor photo and details
# - Chronological movie table (Title, Year, Role, Age, IMDb link, TMDb ID)
# - External links from links.db
# - Biography from <imdb>.bio.db
# Logs messages via Echo, EchoW, EchoD to stdout
#
# Exit Codes:
# 0 - Success
# 1 - Failure to fetch TMDb ID, movie credits, or generate output
#==============================================================================
GenerateActorFilmographyFromTmdb() {
local -r imdb_id="${ACTOR_IMDB_ID}"
local record tmdb_id name dob dod photo_url blog_url
local movie_tmdb_id movie_imdb_id country title original_title release watched_date movie_blogger_url movie_photo_url
local role year age age_cell imdb_link tmdb_link
local output=""
local -r bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db"
DBMovie_InitDb
DBMovie_LoadActorRecordParse "" "${imdb_id}"
EnsureActorMiniBioCached "${imdb_id}"
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: ACTOR_PHOTO_URL='${ACTOR_PHOTO_URL}'"
if [[ -n "${ACTOR_PHOTO_URL}" ]]; then
output+="<div style=\"display:flex; align-items:flex-start; margin-bottom:20px;\">"
output+="<img src=\"${ACTOR_PHOTO_URL}\" alt=\"${ACTOR_NAME}\" style=\"width:160px; margin-right:20px; border:1px solid #ccc;\" />"
output+="<div style=\"font-size:16px; line-height:1.6;\">"
output+="<strong>${ACTOR_NAME}</strong><br />"
output+="Born: ${ACTOR_DOB}<br />"
output+="Died: ${ACTOR_DOD}"
output+="</div></div>"
fi
output+="<h2>Filmography</h2>"
output+="<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">"
output+="<tr><th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th><th>IMDb</th><th>TMDb</th></tr>"
(( VERBOSE==0 )) && Echo "Processing movies. Use --verbose to see each movie name."
(( VERBOSE>0 )) && Echo "Processing movies."
#DBMovie_LoadMovieCreditByActorId "${imdb_id}" > /dev/null
local -r actor_movies="$(DBMovie_LoadMoviesByActor "${ACTOR_TMDB_ID}" | sort -t'|' -k6)"
while IFS='|' read -r movie_tmdb_id movie_imdb_id country title original_title release watched_date movie_blogger_url movie_photo_url; do
(( DEBUG )) && EchoD "movie_tmdb_id='${movie_tmdb_id}'"
role=$(DBMovie_GetMovieRole "${movie_tmdb_id}" "${ACTOR_TMDB_ID}")
year="${release:0:4}"
(( VERBOSE )) && Echo " 🎬 Processing: ${title} (${year})"
# Start of a row
output+="<tr>"
# Movie release year
output+="<td>${year}</td>"
# Movie title, with link to my review in blogger
if [[ -n "${movie_blogger_url}" ]]; then
output+="<td><a href=\"https://afberendsen.blogspot.com/${movie_blogger_url}\" target=\"_blank\">${title}</a></td>"
else
output+="<td>${title}</td>"
fi
output+="<td>${role}</td>"
# Actor age at movie
age=""
age_cell=""
if [[ -n "${ACTOR_DOB}" && -n "${release}" ]]; then
age=$(( ${release:0:4} - ${ACTOR_DOB:0:4} ))
if [[ -n "${ACTOR_DOD}" && "${release}" > "${ACTOR_DOD}" ]]; then
age_cell="❌"
else
age_cell="${age}"
fi
fi
output+="<td>${age_cell}</td>"
# Watched
if [[ -n "${watched_date}" ]]; then
output+="<td>✅</td>"
else
output+="<td></td>"
fi
# IMDb link, if any
if [[ -n "${movie_imdb_id}" ]]; then
output+="<td><a href=\"https://www.imdb.com/title/${movie_imdb_id}\" target=\"_blank\">${movie_imdb_id}</a></td>"
else
output+="<td><span style=\"color:#999;\">N/A</span></td>"
fi
# TMDb link, if any
if [[ -n "${movie_tmdb_id}" ]]; then
output+="<td><a href=\"https://www.themoviedb.org/movie/${movie_tmdb_id}\" target=\"_blank\">${movie_tmdb_id}</a></td>"
else
output+="<td><span style=\"color:#999;\">N/A</span></td>"
fi
# End of row
output+="<tr>"
done <<< "${actor_movies}"
# End of filmography table
output+="</tbody></table>"
# Beginning of Links header
output+="<h2>Links</h2><ul>"
# 1. IMDb actor page
output+="<li><a href=\"https://www.imdb.com/name/${imdb_id}\" target=\"_blank\">IMDb: ${ACTOR_NAME}</a></li>"
# 2. TMDb actor page
if [[ -n "${ACTOR_TMDB_ID}" ]]; then
output+="<li><a href=\"https://www.themoviedb.org/person/${ACTOR_TMDB_ID}\" target=\"_blank\">TMDb: ${ACTOR_NAME}</a></li>"
fi
# 3. Additional custom links from links.db
if [[ -f "${LINKS_DB_PATH}" ]]; then
while read -r dummy url; do
output+="<li><a href=\"${url}\" target=\"_blank\">${url}</a></li>"
done < <(grep -E "^${imdb_id}[[:space:]]+" "${LINKS_DB_PATH}" | sort -u)
else
EchoW "No links.db file found at ${LINKS_DB_PATH}"
fi
output+="</ul>"
output+="<h2>Bio</h2>"
if [[ -f "${bio_file}" ]]; then
output+="$(< "${bio_file}")"
else
EchoW "No biography found at ${bio_file}"
output+="<p><em>Biography not available.</em></p>"
fi
ACTOR_FILMOGRAPHY="${output}"
}
#==============================================================================
# DoActor_0_Filmography: Generate and submit actor filmography to Blogger
#------------------------------------------------------------------------------
# Description:
# This function handles the complete flow of building, previewing, or submitting
# a Blogger post for an actor's filmography. It can use TMDb ID, IMDb ID, or
# actor name as input. Prioritizes local cache and minimal online calls.
#
# Globals Used:
# ACTOR_TMDB_ID, ACTOR_IMDB_ID, ACTOR_NAME
# UPDATE_MODE, DEBUG, DRYRUN
# MY_MDB_CACHE
#
# Globals Set:
# ACTOR_FILMOGRAPHY
#
# Arguments:
# $1 - TMDb actor ID (optional)
# $2 - IMDb actor ID (optional)
# $3 - Actor name (optional)
#
# Return:
# 0 - Success
# 1 - Failure (resolution error, cache error, generation error)
#==============================================================================
DoActor_0_Filmography() {
local -r param_tmdb_id="$(SanitizeField "${1:-}")"
local -r param_imdb_id="$(SanitizeField "${2:-}")"
local -r param_actor_name="$(SanitizeField "${3:-}")"
local -r FUNC="${FUNCNAME[0]}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: $*"
# Validate at least one input
if [[ -z "${param_tmdb_id}" && -z "${param_imdb_id}" && -z "${param_actor_name}" ]]; then
EchoE "${FUNC}:${LINENO}: At least one parameter (TMDB ID, IMDb ID, or Name) must be provided."
return 1
fi
(( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
# Resolve actor IDs
if [[ -n "${param_tmdb_id}" || -n "${param_imdb_id}" ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
ResolveActorIds "${param_tmdb_id}" "${param_imdb_id}" || {
EchoE "${FUNC}:${LINENO}: Failed to resolve actor IDs."
return 1
}
elif [[ -n "${param_actor_name}" ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
GetActorTmdbIdFromName "${param_actor_name}" || {
EchoE "${FUNC}:${LINENO}: Failed to resolve TMDb ID from actor name."
return 1
}
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID=${ACTOR_TMDB_ID}"
if [[ "${ACTOR_TMDB_ID_RESULT}" == "${MANY_RECORD_MARK}" ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID_RESULT == ***MANY***"
if [[ -n "${param_tmdb_id}" ]]; then
ACTOR_TMDB_ID="${param_tmdb_id}"
elif [[ -n "${param_imdb_id}" ]]; then
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID=${ACTOR_TMDB_ID}"
ResolveActorIds "" "${param_imdb_id}" || return 1
else
EchoE "${FUNC}:${LINENO}: Multiple candidates found, but no clear ID to select."
return 1
fi
else
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID=${ACTOR_TMDB_ID}"
ACTOR_TMDB_ID="${ACTOR_TMDB_ID_RESULT}"
fi
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: ACTOR_TMDB_ID=${ACTOR_TMDB_ID}"
ResolveActorIds "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" || return 1
fi
(( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
# Validate IDs are now known
if [[ -z "${ACTOR_TMDB_ID}" || -z "${ACTOR_IMDB_ID}" ]]; then
EchoE "${FUNC}:${LINENO}: Missing TMDb or IMDb ID after resolution."
return 1
fi
(( DEBUG )) && EchoD "${FUNC}:${LINENO}:"
# Confirm actor record exists
DBMovie_LoadActorRecord "${ACTOR_TMDB_ID}" >/dev/null || DBMovie_LoadActorRecord "${ACTOR_IMDB_ID}" >/dev/null || {
EchoE "${FUNC}:${LINENO}: Actor not found in local cache."
return 1
}
DBMovie_LoadActorRecordParse "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}"
Echo "Generating filmography for: ${ACTOR_NAME:-Unknown Actor}"
(( DEBUG )) && EchoD "${FUNC}:${LINENO}: IMDb ID: ${ACTOR_IMDB_ID} TMDb ID: ${ACTOR_TMDB_ID}"
local -r blogger_url="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")"
local new_url=""
HandleNewBloggerPost() {
if [[ -n "${new_url}" ]]; then
Echo "🌐 Blogger URL: ${new_url}"
if (( DRYRUN == 0 )); then
DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_url#*blogspot.com/}"
else
Echo "DRYRUN active: skipping Blogger URL cache update."
fi
else
EchoE "${FUNC}:${LINENO}: Failed to create Blogger post."
return 1
fi
}
if [[ -n "${blogger_url}" && ${UPDATE_MODE} -eq 0 ]]; then
while true; do
Echo "Existing Blogger URL found: ${blogger_url}"
Echo "Choose one of the following options:"
Echo " a) Copy HTML to clipboard"
Echo " b) Save HTML locally as ${ACTOR_IMDB_ID}.html"
Echo " c) Create a new Blogger post and update cache"
Echo " d) Cancel"
EchoN "Your choice (a/b/c/d): "
read -r choice
choice="${choice:0:1}"
choice="${choice,,}"
choice="${choice// /}"
case "${choice}" in
a)
GenerateActorFilmographyFromTmdb || return 1
echo "${ACTOR_FILMOGRAPHY}" > /dev/clipboard
Echo "Copied HTML to clipboard."
return 0
;;
b)
GenerateActorFilmographyFromTmdb || return 1
echo "${ACTOR_FILMOGRAPHY}" > "${ACTOR_IMDB_ID}.html"
Echo "Saved HTML to ${ACTOR_IMDB_ID}.html"
return 0
;;
c)
GenerateActorFilmographyFromTmdb || return 1
new_url="$(SubmitBlogDraft "${ACTOR_FILMOGRAPHY}")"
HandleNewBloggerPost || return 1
break
;;
d)
Echo "Aborted by user."
return 1
;;
*)
EchoE "Invalid choice '${choice}'. Try again."
;;
esac
done
else
# No URL or forced update
GenerateActorFilmographyFromTmdb || return 1
new_url="$(SubmitBlogDraft "${ACTOR_FILMOGRAPHY}")"
HandleNewBloggerPost || return 1
fi
Echo "✅ Done: Post created or updated for IMDb ID '${ACTOR_IMDB_ID}'."
return 0
}
#==============================================================================
# DoActor_1_ShowUrl: Display the current Blogger URL for a given actor
#------------------------------------------------------------------------------
# Description:
# This function queries the actor.db cache to retrieve the Blogger URL
# associated with the IMDb ID stored in ACTOR_IMDB_ID. It outputs the URL
# directly, or a blank if no entry exists.
#
# Globals Used:
# ACTOR_IMDB_ID - IMDb ID of the actor (used as lookup key)
# ACTOR_DB - Full path to ~/.MyBlogCache/actor.db
#
# Outputs:
# Echoes the Blogger URL to stdout if it exists, otherwise a blank URL notice.
#
# Exit Codes:
# 0 - Always succeeds; informational only
#==============================================================================
DoActor_1_ShowUrl() {
local blogger_url
blogger_url="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")"
if [[ -n "${blogger_url}" ]]; then
Echo "Current Blogger URL is '${blogger_url}'"
else
Echo "Current Blogger URL is ''"
fi
}
#==============================================================================
# DoActor_2_DeleteUrl: Remove the Blogger URL associated with an actor
#------------------------------------------------------------------------------
# Description:
# This function clears the Blogger URL field for the actor identified by
# ACTOR_IMDB_ID in the actor.db cache file. It effectively disassociates
# the actor from any previously posted Blogger URL.
#
# Globals Used:
# ACTOR_IMDB_ID - IMDb ID of the actor (used as lookup key)
# ACTOR_DB - Full path to ~/.MyBlogCache/actor.db (used internally)
# actor_name - Display name for echo output (optional, used for clarity)
#
# Outputs:
# Echoes a confirmation message indicating the Blogger URL was cleared.
#
# Exit Codes:
# 0 - Always succeeds; informational only
#==============================================================================
DoActor_2_DeleteUrl() {
Echo "Clearing Blogger URL for '${actor_name}'..."
DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" ""
}
#==============================================================================
# DoActor_3_UpdateUrl: Update Blogger URL for actor and validate uniqueness
#------------------------------------------------------------------------------
# Description:
# Updates the Blogger URL for the actor identified by ACTOR_IMDB_ID.
# Ensures the new URL is valid, unique across all actors, and optionally
# allows overwriting identical entries if FORCE is set. Logs all updates to
# ~/.MyMDBCache/actor_url_changes.log for auditing.
#
# Globals Used:
# ACTOR_IMDB_ID - IMDb ID of the actor (used as the key)
# ACTOR_DB - Full path to actor.db (used to check uniqueness)
# actor_name - Actor's name (used for logging/display)
# FORCE - If non-zero, allows update even if path is unchanged
#
# Arguments:
# ${1} - New full Blogger URL (must start with https:// and contain blogspot.com)
#
# Outputs:
# - Confirms update via Echo
# - Logs change to ~/.MyMDBCache/actor_url_changes.log
# - Warns or errors on malformed or duplicate URLs
#
# Exit Codes:
# 0 - Success or no change needed
# 1 - Invalid URL or duplication detected
#==============================================================================
DoActor_3_UpdateUrl() {
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: 1='${1}'"
local new_url="${1}"
if [[ -z "${new_url}" ]]; then
EchoE "Missing Blogger URL for update."
exit 1
fi
if [[ "${new_url}" != https://*blogspot.com/* ]]; then
EchoE "Only HTTPS blogspot URLs are allowed."
exit 1
fi
local new_path="${new_url#*blogspot.com/}"
new_path="${new_path%%/}" # Trim trailing slashes
if [[ "${new_path}" == "${new_url}" ]]; then
EchoE "Invalid Blogger URL format: ${new_url}"
exit 1
fi
if [[ "${new_path}" =~ [[:space:]] ]]; then
EchoW "Blogger path contains spaces; this may indicate a malformed URL."
fi
local current_path
current_path="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")"
current_path="${current_path%%/}"
if [[ "${new_path,,}" == "${current_path,,}" ]]; then
if (( FORCE )); then
EchoD "FORCE active: proceeding with update of identical path."
else
EchoW "New URL is same as current; skipping update..."
return 0
fi
fi
# Check if another actor is using this path
local path_owner
path_owner="$(grep -i "|${new_path}$" "${ACTOR_DB}" | awk -F'|' '$1 != "'${ACTOR_IMDB_ID}'" {print $3}')"
if [[ -n "${path_owner}" ]]; then
EchoE "Another actor already uses this Blogger path: ${path_owner}"
exit 1
fi
Echo "🌐 Updating Blogger URL for '${actor_name}'..."
Echo " Previous: '${current_path}'"
Echo " New : '${new_path}'"
# Save history
local log="${HOME}/.MyMDBCache/actor_url_changes.log"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "${timestamp}|${ACTOR_IMDB_ID}|${actor_name}|${current_path}|${new_path}" >> "${log}"
DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_path}"
Echo "🌐 Current Blogger URL is '${new_path}'"
}
#==============================================================================
# DoActor_4_RevertUrl: Revert Blogger URL for actor from history
#------------------------------------------------------------------------------
# Description:
# Reverts the Blogger URL associated with an actor (identified by ACTOR_IMDB_ID)
# to a previous value using the actor URL change log at
# ~/.MyMDBCache/actor_url_changes.log. The number of steps to revert can be
# specified; default is 1 (revert to immediately previous URL).
#
# Globals Used:
# ACTOR_IMDB_ID - IMDb ID of the actor (used to identify history lines)
# HOME - Used to locate the change history log under ~/.MyMDBCache
#
# Arguments:
# ${1} - (Optional) Number of steps to revert back (default is 1)
#
# Outputs:
# - Echo message indicating new (reverted) Blogger URL
# - Error messages if log file is missing, entry not found, or step is invalid
#
# Exit Codes:
# 0 - Successful revert
# 1 - Error: log file missing, insufficient history, or malformed log entry
#==============================================================================
DoActor_4_RevertUrl() {
local -i steps="${1:-1}"
local log="${HOME}/.MyMDBCache/actor_url_changes.log"
local -i count=0
if [[ ! -f "${log}" ]]; then
EchoE "No history log found."
exit 1
fi
local lines
lines="$(grep "|${ACTOR_IMDB_ID}|" "${log}" | tac)"
if [[ -z "${lines}" ]]; then
EchoE "No history for this actor."
exit 1
fi
while IFS= read -r line; do
(( count++ ))
if (( count == steps )); then
local prev_path
prev_path="$(echo "${line}" | cut -d'|' -f4)"
if [[ -z "${prev_path}" ]]; then
EchoE "Previous URL path not found in history."
exit 1
fi
Echo "Rolling back to '${prev_path}'..."
DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${prev_path}"
return 0
fi
done < <(echo "${lines}")
EchoE "Unable to revert: fewer than ${steps} history entries found."
exit 1
}
#==============================================================================
# DoActor_5_ShowUrlHistory: Display change history for actor Blogger URLs
#------------------------------------------------------------------------------
# Description:
# Displays the timestamped history of Blogger URL changes for a specific actor
# using the IMDb ID defined in ACTOR_IMDB_ID. It parses the
# ~/.MyMDBCache/actor_url_changes.log file and presents each historical update
# as a timestamped transition from old to new URL path.
#
# Globals Used:
# ACTOR_IMDB_ID - IMDb ID of the actor (used to match log entries)
# HOME - Used to resolve log path at ~/.MyMDBCache/
#
# Outputs:
# - Prints timestamp, previous URL path, and updated URL path for each entry
# - EchoE warning if the log file is missing
#
# Exit Codes:
# 0 - Success (entries printed or empty result)
# 1 - Error: history log not found
#==============================================================================
DoActor_5_ShowUrlHistory() {
local log="${HOME}/.MyMDBCache/actor_url_changes.log"
if [[ ! -f "${log}" ]]; then
EchoE "No history file found."
exit 1
fi
grep "|${ACTOR_IMDB_ID}|" "${log}" | while IFS='|' read -r ts imdb name old new; do
printf "🕓 %s: %s → %s\n" "${ts}" "${old}" "${new}"
done
}
#==============================================================================
# DoActor_6_ShowPosterUrl: Show the current photo URL associated with an actor
#------------------------------------------------------------------------------
# Globals:
# ACTOR_IMDB_ID - IMDb ID of the actor
# ACTOR_DB - Path to actor.db
# Outputs:
# Prints current photo URL for the actor or a notice if none exists
#==============================================================================
DoActor_6_ShowPosterUrl() {
local photo_url
photo_url="$(DBMovie_GetActorPhotoUrl "${ACTOR_IMDB_ID}")"
if [[ -n "${photo_url}" ]]; then
Echo "Current photo URL is '${photo_url}'"
else
Echo "Current photo URL is ''"
fi
}
#==============================================================================
# DoActor_7_DeletePosterUrl: Remove the photo URL associated with an actor
#------------------------------------------------------------------------------
# Globals:
# ACTOR_IMDB_ID - IMDb ID of the actor
# Outputs:
# Deletes photo URL field for actor in cache and confirms the action
#==============================================================================
DoActor_7_DeletePosterUrl() {
Echo "Clearing photo URL for '${actor_name}'..."
DBMovie_UpdateActorPhotoUrl "${ACTOR_IMDB_ID}" ""
}
#==============================================================================
# DoActor_8_UpdatePosterUrl: Update photo URL for actor and validate uniqueness
#------------------------------------------------------------------------------
# Globals:
# ACTOR_IMDB_ID - IMDb ID of the actor
# FORCE - If set, allow identical URL update
# ACTOR_DB - Path to actor.db
# Arguments:
# ${1} - New full photo URL (must start with https://)
# Outputs:
# Updates photo URL for the actor, logs change history, and prints new state
#==============================================================================
DoActor_8_UpdatePosterUrl() {
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: 1='${1}'"
local new_url="${1}"
if [[ -z "${new_url}" ]]; then
EchoE "Missing photo URL for update."
exit 1
fi
if [[ "${new_url}" != https://* ]]; then
EchoE "Only HTTPS URLs are allowed."
exit 1
fi
local current_url
current_url="$(DBMovie_GetActorPhotoUrl "${ACTOR_IMDB_ID}")"
if [[ "${new_url}" == "${current_url}" ]]; then
if (( FORCE )); then
EchoD "FORCE active: proceeding with update of identical photo URL."
else
EchoW "New URL is same as current; skipping update..."
return 0
fi
fi
Echo "🖼️ Updating photo URL for '${actor_name}'..."
Echo " Previous: '${current_url}'"
Echo " New : '${new_url}'"
local log="${HOME}/.MyMDBCache/actor_photo_changes.log"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "${timestamp}|${ACTOR_IMDB_ID}|${actor_name}|${current_url}|${new_url}" >> "${log}"
DBMovie_UpdateActorPhotoUrl "${ACTOR_IMDB_ID}" "${new_url}"
Echo "🖼️ Current photo URL is '${new_url}'"
}
#==============================================================================
# DoActor_9_RevertPosterUrl: Revert actor photo URL from history
#------------------------------------------------------------------------------
# Globals:
# ACTOR_IMDB_ID - IMDb ID of the actor
# HOME - Used to locate history log at ~/.MyMDBCache/
# Arguments:
# ${1} - Number of steps to go back (default: 1)
# Outputs:
# Reverts actor photo URL if history available and prints result
#==============================================================================
DoActor_9_RevertPosterUrl() {
local -i steps="${1:-1}"
local log="${HOME}/.MyMDBCache/actor_photo_changes.log"
local -i count=0
if [[ ! -f "${log}" ]]; then
EchoE "No photo URL history log found."
exit 1
fi
local lines
lines="$(grep "|${ACTOR_IMDB_ID}|" "${log}" | tac)"
if [[ -z "${lines}" ]]; then
EchoE "No photo URL history for this actor."
exit 1
fi
while IFS= read -r line; do
(( count++ ))
if (( count == steps )); then
local prev_url
prev_url="$(echo "${line}" | cut -d'|' -f4)"
if [[ -z "${prev_url}" ]]; then
EchoE "Previous photo URL not found in history."
exit 1
fi
Echo "Rolling back to '${prev_url}'..."
DBMovie_UpdateActorPhotoUrl "${ACTOR_IMDB_ID}" "${prev_url}"
return 0
fi
done < <(echo "${lines}")
EchoE "Unable to revert: fewer than ${steps} history entries found."
exit 1
}
#==============================================================================
# DoActor_A_ShowPosterUrlHistory: Display change history for actor photo URLs
#------------------------------------------------------------------------------
# Globals:
# ACTOR_IMDB_ID - IMDb ID of the actor
# HOME - Used to locate history log at ~/.MyMDBCache/
# Outputs:
# Displays all historical photo URL changes for the specified actor
#==============================================================================
DoActor_A_ShowPosterUrlHistory() {
local log="${HOME}/.MyMDBCache/actor_photo_changes.log"
if [[ ! -f "${log}" ]]; then
EchoE "No photo URL history file found."
exit 1
fi
grep "|${ACTOR_IMDB_ID}|" "${log}" | while IFS='|' read -r ts imdb name old new; do
printf "🕓 %s: %s → %s\n" "${ts}" "${old}" "${new}"
done
}
#==============================================================================
# DoActor: Executes actor-related operations based on submode
#------------------------------------------------------------------------------
# Description:
# Routes execution to one of the DoActor_* functions depending on the selected
# EXECUTION_SUBMODE. This is the main dispatcher for actor-related operations,
# including generating filmography HTML, showing or updating Blogger URLs,
# reverting URL changes, and displaying change history.
#
# Globals Used:
# EXECUTION_SUBMODE - Mode selector (0=create, 1=show URL, 2=delete, 3=update, 4=revert, 5=history)
# ACTOR_QUERY - Name of the actor (may be overridden by IMDB_ID)
# IMDB_ID - Optional IMDb ID of the actor (takes precedence over ACTOR_QUERY)
# ACTOR_IMDB_ID - Set internally after resolving actor identity
# NEW_BLOGGER_URL - Used in update mode to set new Blogger path
# REVERT_STEPS - Used in revert mode to control rollback depth
# DEBUG - Enables diagnostic logging
#
# Arguments:
# ${1} - Submode to execute (0 = filmography, 1 = show, 2 = delete, 3 = update, 4 = revert, 5 = history)
# ${2} - Optional argument depending on the submode (e.g., new URL or number of steps)
#
# Exit Codes:
# 0 - Success
# 1 - Failure (e.g., unknown mode, failed resolution, missing actor record)
#==============================================================================
DoActor() {
(( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: $*"
local -r mode="${1:-0}"
# Ensure DBs are initialized
DBMovie_InitDb || return 1
# # Determine which input to use
# local actor_ref=""
# if [[ -n "${IMDB_ID}" ]]; then
# actor_ref="${IMDB_ID}"
# elif [[ -n "${ACTOR_QUERY}" ]]; then
# actor_ref="${ACTOR_QUERY}"
# else
# EchoE "${FUNCNAME[0]}:${LINENO}: No actor reference (IMDB_ID or ACTOR_QUERY) provided."
# return 1
# fi
#
# # Try resolving TMDb/IMDb IDs
# ResolveActorIdFromNameOnline "${actor_ref}" || {
# EchoE "${FUNCNAME[0]}:${LINENO}: Failed to resolve actor identity for '${actor_ref}'."
# return 1
# }
#
# # Now ACTOR_TMDB_ID and ACTOR_IMDB_ID must be filled
# if [[ -z "${ACTOR_TMDB_ID}" || -z "${ACTOR_IMDB_ID}" ]]; then
# EchoE "${FUNCNAME[0]}:${LINENO}: Missing TMDb or IMDb ID after resolution."
# return 1
# fi
#
# # Ensure actor record is present locally
# DBMovie_LoadActorRecord "${ACTOR_TMDB_ID}" || DBMovie_LoadActorRecord "${ACTOR_IMDB_ID}" || {
# EchoE "${FUNCNAME[0]}:${LINENO}: Actor not found in local cache (TMDb ${ACTOR_TMDB_ID} or IMDb ${ACTOR_IMDB_ID})."
# return 1
# }
# Dispatch to the appropriate mode handler
case "${mode}" in
0) DoActor_0_Filmography "${ACTOR_TMDB_ID}" "${ACTOR_IMDB_ID}" "${ACTOR_QUERY}" ;;
1) DoActor_1_ShowUrl "${2}" ;;
2) DoActor_2_DeleteUrl "${2}" ;;
3) DoActor_3_UpdateUrl "${2}" ;;
4) DoActor_4_RevertUrl "${2}" ;;
5) DoActor_5_ShowUrlHistory "${2}" ;;
6) DoActor_6_ShowPosterUrl "${2}" ;;
7) DoActor_7_DeletePosterUrl "${2}" ;;
8) DoActor_8_UpdatePosterUrl "${2}" ;;
9) DoActor_9_RevertPosterUrl "${2}" ;;
10) DoActor_A_ShowPosterUrlHistory "${2}" ;;
*)
EchoE "${FUNCNAME[0]}:${LINENO}: Invalid execution mode '${mode}'."
return 1
;;
esac
return 0
}
#==============================================================================
# DoMovieList: Scans movie directories and posts updates to Blogger
#------------------------------------------------------------------------------
# Description:
# Determines the effective REFERENCE_DATE (either today's date or CUSTOM_DATE
# passed via --date). Scans for updates to movie directories, filters results
# by year if applicable, builds HTML tables and summaries, and submits a
# Blogger draft post containing the update.
#
# Globals Used:
# CUSTOM_DATE - Optional override for the reference date
# FORCE_PUBLISH - If set, overrides date/year warnings
# FORCE - Bypasses user confirmation prompt
# FILTER_YEARS[] - Optional list of years to filter updates
# REFERENCE_DATE - Set here based on CUSTOM_DATE or today
# REFERENCE_DATE_EPOCH - Epoch timestamp for REFERENCE_DATE
# TODAY_EPOCH - Epoch timestamp for REFERENCE_DATE + 1 day
# year_map[] - Filled by ScanDirectories with year → updated movies
#
# Outputs:
# Blogger draft post submission with updated titles and summary
#
# Exit Codes:
# 0 - Success
# 1 - Invalid date format or aborted by user
#==============================================================================
DoMovieList() {
# Validate and calculate effective date
if [[ -z "${CUSTOM_DATE}" ]]; then
EFFECTIVE_DATE=$(date --date=REFERENCE_DATE +%Y-%m-%d)
else
if ! date --date="${CUSTOM_DATE}" &>/dev/null; then
Echo "ERROR: Invalid date format for --date: ${CUSTOM_DATE}"; exit 1
fi
CUSTOM_EPOCH=$(date --date="${CUSTOM_DATE} 00:00:00" +%s)
TODAY_EPOCH=$(date --date="today 00:00:00" +%s)
if (( CUSTOM_EPOCH >= TODAY_EPOCH )); then
Echo "ERROR: Specified date must be before today."; exit 1
fi
EFFECTIVE_DATE=$(date --date="${CUSTOM_DATE}" +%Y-%m-%d)
fi
readonly REFERENCE_DATE="${EFFECTIVE_DATE}"
readonly REFERENCE_DATE_EPOCH=$(date --date="${EFFECTIVE_DATE} 00:00:00" +%s)
readonly TODAY_EPOCH=$(date --date="${EFFECTIVE_DATE} 00:00:00 + 1 day" +%s)
if (( FORCE_PUBLISH == 1 && FORCE == 0 )); then
if (( FORCE_PUBLISH == 1 && FORCE == 0 && ${#FILTER_YEARS[@]} > 0 )); then
EchoW "⚠️ WARNING: You specified --publish AND limited the year(s) with --year/--range."
EchoW " The post will not reflect updates from all years. Publishing it could be misleading."
EchoW " Use --force to suppress this warning."
read -p "Do you still want to proceed? (y/n): " answer
[[ ! "${answer}" =~ ^[Yy]$ ]] && { Echo "Aborting by user request."; exit 1; }
fi
fi
GetBlogAccessToken || {
EchoE "Failed to retrieve Blogger API access token."
return 1
}
ValidateTmdbKey
ScanDirectories
if [[ ${#year_map[@]} -eq 0 ]]; then
Echo "No updated titles found for ${REFERENCE_DATE}."
else
html_output="$(BuildHtmlTable)$(BuildSummaryHtml)"
SubmitBlogDraft "${html_output}"
Echo "Done."
fi
}
#==============================================================================
# Main: Entry point of script. Parses arguments and dispatches mode logic
#------------------------------------------------------------------------------
# Description:
# Main dispatcher for the script. Parses all command-line arguments, validates
# dependencies, and routes to the appropriate execution mode (movie or actor).
# Supports multiple submodes for actor operations such as filmography posting,
# URL management, and history reversion.
#
# Globals:
# EXECUTION_MODE - Primary mode of operation (1=movie-list, 2=actor)
# EXECUTION_SUBMODE - Actor submode (0–5)
# NEW_BLOGGER_URL - Passed to actor submode 3 (update)
# REVERT_STEPS - Passed to actor submode 4 (revert)
# DRYRUN - If set, suppresses blog submission
#
# Arguments:
# All command-line parameters passed to the script
#
# Exit Codes:
# 0 - Success
# 1 - Invalid mode or submode, or argument parsing failure
#==============================================================================
Main() {
if DoDependency && ParseArguments "$@" && DBMovie_InitDb; then
case ${EXECUTION_MODE} in
1) RunMovieUpdate ;; # Movie list mode
2) case ${EXECUTION_SUBMODE} in
0|1|2|5|6|7|10) DoActor ${EXECUTION_SUBMODE} ;;
3) DoActor ${EXECUTION_SUBMODE} "${NEW_BLOGGER_URL}" ;;
4) DoActor ${EXECUTION_SUBMODE} "${REVERT_STEPS:-1}" ;;
8) DoActor ${EXECUTION_SUBMODE} "${NEW_PHOTO_URL}" ;;
9) DoActor ${EXECUTION_SUBMODE} "${REVERT_STEPS:-1}" ;;
*) EchoE "Invalid --actor execution mode ${EXECUTION_SUBMODE}" ; exit 1 ;;
esac
;;
*) EchoE "Invalid execution mode" ; exit 1 ;;
esac
return 0
fi
return 1
}
Main "$@"
exit $?
# -----------------------------------------------------------------------------
# __CHATGPT_INSTRUCTIONS_BEGIN__
#
# All Bash functions in this script must include a header block with the following format:
#
#==============================================================================
# <function-name> - <short description of what the function does>
# Globals:
# <VARIABLE_NAME> - Description of its use (RO for read-only, RW for read-write)
# ...
# Arguments:
# $1 - Description of the first positional argument
# $2 - Description of the second positional argument (if applicable)
# ...
# Outputs:
# <Clearly state what is written to stdout or stderr>
# <Any global variables modified (e.g., sets ACTOR_FILMOGRAPHY)>
#==============================================================================
#
# Guidelines:
# - Always use `local` for function-local variables.
# - Use `${...}` consistently for all variable references.
# - Output user-facing messages using the Echo, EchoE, EchoW, EchoD functions.
# - When writing a function that returns data via stdout, prefer using a dedicated file descriptor (e.g., `>&5`) to avoid collisions with logging.
# - Each function should be self-contained and document every global variable it accesses or modifies.
# - Always wrap long `jq` or `curl` commands in clean, readable syntax with fallbacks or clear error handling.
#
# Logging functions are predefined and must be used instead of raw `echo`.
# Each automatically includes a timestamp, an icon, and a log-level prefix.
#
# Use the following functions consistently:
#
# - `Echo` → for normal informational output (✅ or 📌)
# - `EchoW` → for warnings (⚠️ WARNING:)
# - `EchoE` → for errors (❌ ERROR:)
# - `EchoD` → for debug output (🛠 DEBUG:)
#
# Important:
# - Do **NOT** prepend messages manually with "ERROR:", "WARNING:", or emojis — these are automatically included by the functions.
# - Do **NOT** call `echo` directly for user-facing output unless it's data being piped, stored, or exported.
# - All log messages should be clear, human-readable, and aligned with script actions.
#
# Examples (✅ correct):
# Echo "✅ Submitted successfully." # icon can be supplied
# EchoW "Actor already exists in the database."
# EchoE "Could not resolve TMDb ID for IMDb ID '${id}'."
# EchoD "Parsed movie title: ${title}"
#
# Examples (❌ incorrect):
# EchoW "WARNING: Actor exists." # redundant prefix
# EchoE "❌ ERROR: Failed to post." # message is duplicated
# EchoD "2025-04-22 00:51:35| Parsed movie title: ${title}" # message havs two time stamps
#
# 1. **Purpose and Modes**:
# - The script supports four modes: --delete, --recheck-error, --set-priority, and --manage-downloads.
# - --delete: Deletes torrents matching patterns in TV_Organize_Patterns.txt.
# - --recheck-error: Rechecks torrents in an "error" state for a specific qBittorrent instance.
# - --set-priority: Sets high priority and force-starts TV torrents matching specific prefixes.
# - --manage-downloads: Moves completed torrents to a final directory and ensures incomplete ones are in a temp directory.
#
# 2. **Key Variables**:
# - PREFIXES: Array of TV show prefixes used for matching in --set-priority mode.
# - QBT_INSTANCES: Associative array mapping instance names (tv, anime, etc.) to qBittorrent ports.
# - TEMP_DIRS/FINAL_DIRS: Directory mappings for --manage-downloads mode.
# - DRY_RUN: Flag to simulate actions (default: 1, enabled).
#
# 3. **Dependencies**:
# - Requires curl, jq, date, and urlencode (with fallbacks for urlencode).
# - Dependency checks are configurable via --check-deps, --check-deps-local, --check-deps-install.
#
# 4. **Logging and Debugging**:
# - Use --debug (-D) to enable verbose output, including curl commands and intermediate data.
# - Logs are written to /cygdrive/f/p_qBittorrent/.logs for reachability and failures.
#
# 5. **Error Handling**:
# - The script includes reachability checks for qBittorrent with retries.
# - Temporary files are cleaned up on exit (via trap).
# - API responses are checked for failures.
#
# 6. **Potential Improvements**:
# - Add support for more qBittorrent API endpoints (e.g., pausing/resuming torrents).
# - Enhance pattern matching with regex options in --delete mode.
# - Add a --force option to skip user prompts in --delete mode.
#
# 7. **Known Issues**:
# - jq 1.7.1 has a bug causing assertion failures with complex expressions. Workarounds include simplifying jq usage or upgrading to a newer version.
# - Large torrent lists may cause performance issues; consider batching API calls.
#
# __CHATGPT_INSTRUCTIONS_END__
# -----------------------------------------------------------------------------
Comments
Post a Comment