#!/usr/bin/env bash [[ "$(uname -a)" != *CYGWIN_NT* ]] && echo "This script can only be executed under Cygwin" && exit 1 # 🧠 ChatGPT-aware developer instructions at bottom of script (__CHATGPT_INSTRUCTIONS_BEGIN__) ################################################################################ # Script Name: MovieUpdateDraft.sh # Description: Scans movie directories for files updated on a specific date, # grouped by year, and submits a DRAFT post to Blogger with a table of changes. # # Version History: # v1.0 - 2025-04-16 - Initial version # v1.1 - 2025-04-16 - Fixed Blogger API JSON parse error (HTML table incorrectly escaped) # v1.2 - 2025-04-16 - Replaced literal '\n' with <br> in HTML content # v1.3 - 2025-04-16 - Removed empty <li> element from bullet list # v1.4 - 2025-04-16 - Added intro paragraph above the table describing update date # v1.5 - 2025-04-16 - Formatted table header (blue background, white bold font) # v1.6 - 2025-04-16 - Inserted HTML comment identifying generating script # v1.7 - 2025-04-16 - Fixed invalid "REFERENCE_DATE" parsing bug (date CLI error) # v1.8 - 2025-04-16 - Replaced "(updated REFERENCE_DATE)" with actual date text "(updated YYYY-MM-DD)" # v1.9 - 2025-04-16 - Added support for --date and --debug switches # v1.10 - 2025-04-16 - Adjusted verbosity logic for no-match and matched file output # v1.11 - 2025-04-16 - Controlled "Checking title" debug echo using DEBUG flag # v1.12 - 2025-04-16 - Sorted matched titles alphabetically within each year group # v1.13 - 2025-04-16 - Added distinction between "added" vs. "changed" titles (based on presence of metadata.json) # v1.14 - 2025-04-16 - Separated logic for new/changed titles into HTML output structure # v1.15 - 2025-04-16 - Improved OMDb cache initialization and format validation # v1.16 - 2025-04-16 - Implemented IMDb runtime and year consistency checks # v1.17 - 2025-04-16 - Added collapsible file list support for each title row # v1.18 - 2025-04-16 - Added support for multiple years using --year (cumulative) # v1.19 - 2025-04-16 - Introduced optional HTML export toggle with --no-html # v1.20 - 2025-04-16 - Appended update type (Added vs Changed) as suffix in bullet list # v1.21 - 2025-04-16 - Finalized consistent year traversal and grouping logic # v1.22 - 2025-04-16 - Optimised local OMDb metadata caching with minimal lookups # Version History: # v2.0 - 2025-04-16 - 🟦 Major release: integrated OMDb API, poster images, runtime, categories, collapsible UI, HTML export, Blogger DRAFT formatting, and comprehensive update type detection # v2.1 - 2025-04-16 - Added --year (-y) and --range (-r) options to limit processing by year or year range # v2.2 - 2025-04-16 - Changed YESTERDAY to REFERENCE_DATE throughout for clarity and accuracy # v2.3 - 2025-04-16 - Added --publish flag to enable automatic scheduling as LIVE post # v2.4 - 2025-04-16 - Added retry logic for transient curl DNS failures (curl error 6: Could not resolve host) # v2.5 - 2025-04-16 - Fixed UTC scheduling issue by posting at 21:59 UTC to appear as 23:59 local # v2.6 - 2025-04-16 - Added support for --title (-t) to override the default post title, and --label (-l) to add one or more Blogger labels # v2.7 - 2025-04-16 - Added --force (-f) to skip confirmation warning when using --publish with filtered years # v2.8 - 2025-04-16 - Token retrieval now retries indefinitely on failure with 2s backoff and visual feedback # v2.9 - 2025-04-16 - Fixed logic bug in ScanDirectories(): OMDb was being called outside of loop context # v2.10 - 2025-04-16 - Optimized OMDb API calls: lookup now occurs only for matched folders with updates # v2.11 - 2025-04-16 - Appended OMDb genres in bold purple [Genre] to each title entry in the post (within <li>) # v2.12 - 2025-04-16 - Added clickable IMDb links for each title using OMDb imdbID metadata # v2.13 - 2025-04-16 - Appended runtime (e.g. 103 min) to genre tag for each title using OMDb metadata # v2.14 - 2025-04-16 - Integrated TMDb actor lookup: new GetTopActorsFromTmdb() with name, role, and age (based on release year) # v2.15 - 2025-04-16 - Introduced collapsible cast block using <details> + <summary> for cleaner blog display # v2.16 - 2025-04-16 - Cast block supports up to 7 actors; age calculation based on release year minus birth year # v2.17 - 2025-04-16 - Fixed bug: all Echo* output now routed via FD 10/11 to prevent HTML output contamination # v2.18 - 2025-04-16 - TMDb key is now validated automatically at script start; --force-actors overrides failures # v2.19 - 2025-04-16 - Blog post generation now correctly sorts titles alphabetically within each year # v2.20 - 2025-04-16 - Fixed multibyte sort bug (e.g., emojis in <summary>) using LC_ALL=C # v2.21 - 2025-04-19 - Refactored --check-deps to support default, local-only, and install modes # # Version History: # v3.0 - 2025-04-19 - 🎭 Major release: implemented --actor mode for generating actor blog posts using TMDb/OMDb metadata # v3.1 - 2025-04-19 - Added support for loading actor metadata from actor.db; fallback to TMDb if not cached # v3.2 - 2025-04-20 - Implemented movie credits caching via movie_credits.db and chronological role sorting # v3.3 - 2025-04-20 - Appended IMDb and TMDb movie links to filmography entries; fallback for missing codes as N/A # v3.4 - 2025-04-21 - Introduced movie.db with support for title, release date, original title, and country # v3.5 - 2025-04-21 - Added support for external link sections via links.db and biography via cached HTML # v3.6 - 2025-04-22 - Added WATCHED_DATE field to movie.db; Watch column now displays ✅ icon if set # v3.7 - 2025-04-22 - Introduced DBMovie_GetMovieWatchedDate; improved fallback logic for empty IMDb/TMDb codes # v3.8 - 2025-04-22 - Standardized headers across all DBMovie_* and DoActor_* functions; updated SaveMovieMetadata to support optional watched date # v3.9 - 2025-04-22 - Added MarkMovieAsWatched() helper function to update watch status in movie.db by TMDb ID # # Version History: # v4.0 - 2025-04-23 - 🎬 Extended movie.db format with BLOGGER_URL and PHOTO_URL fields; DBMovie_SaveMovieMetadata updated accordingly #============================================================================== ################################################################################ #grep -E '^\s*(function\s+)?[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)\s*\{' MovieUpdateDraft.sh | sed -E 's/^\s*(function\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\).*/\2/' | sort # Load shared library [[ ! -e ~/MY_LIBRARY.sh ]] && { echo "***ERROR: Cannot find library" ; exit 1; } [[ ! -r ~/MY_LIBRARY.sh ]] && { echo "***ERROR: Library is not readable" ; exit 1; } source ~/MY_LIBRARY.sh || { echo "***ERROR: Cannot source library" ; exit 1; } [[ ! -e ~/MY_SECRETS.sh ]] && { echo "***ERROR: Cannot find ~/MY_SECRETS.sh" ; exit 1; } [[ ! -r ~/MY_SECRETS.sh ]] && { echo "***ERROR: ~/MY_SECRETS.sh is not readable" ; exit 1; } source ~/MY_SECRETS.sh || { echo "***ERROR: Cannot source ~/MY_SECRETS.sh" ; exit 1; } set -o monitor trap 'echo;Echo "💥 Caught interrupt signal, exiting."; exit 130' SIGINT SIGTERM # Constants readonly SCRIPT_VERSION="3.9" readonly MOVIE_ROOT="/cygdrive/f/MediaLibrary/Movies/!By_Year" # Variables declare -A year_map=() declare -a FILTER_YEARS=() declare -a LABELS=() declare -i VERBOSE=0 declare -i VERBOSE_DEPS=0 declare -i DRYRUN=0 declare -i DEBUG=0 declare -a POSITIONAL_ARGS=() declare -i TMDB_KEY_VALID=0 declare -i CHECK_DEPS_DEFAULT=0 declare -i CHECK_DEPS_LOCAL=1 declare -i CHECK_DEPS_INSTALL=0 declare -i DRY_RUN=0 declare -i EXECUTION_MODE=0 # 0 = undefined, 1 = movie list, 2 = actor mode, etc. declare -i EXECUTION_SUBMODE=0 #----- Movie List Variables declare CUSTOM_DATE="" declare -i USE_HTML=1 declare -i FORCE_PUBLISH=0 declare CUSTOM_TITLE="" declare -i FORCE=0 declare -i FORCE_ACTORS=0 #----- ACTOR mode variable declare ACTOR_QUERY="" declare -i UPDATE_MODE=0 declare ACTOR_UPDATE_PATH="" declare IMDB_ID="" declare ACTOR_QUERY="" declare ACTOR_IMDB_ID="" declare NEW_BLOGGER_URL= declare -gi FORCE_BIO=0 # Metadata Cache declare -A METADATA_CACHE=() readonly METADATA_FILE_NAME="metadata.json" readonly OMDB_CACHE_FILE="/tmp/.omdb_cache.json" #*************************************************************************************** #*************************** Local Movie Database cache functions ********************** #*************************************************************************************** #************************* This code should be move to MY_DB.sh later. It wil be sourced # Ensure local cache folder exists # ============================================================================== # Cache location definitions # ============================================================================== readonly MY_MDB_CACHE="${HOME}/.MyMDBCache" readonly ACTOR_DB_PATH="${MY_MDB_CACHE}/actor.db" readonly MOVIE_DB_PATH="${MY_MDB_CACHE}/movie.db" readonly MOVIE_CREDITS_DB_PATH="${MY_MDB_CACHE}/movie_credits.db" #============================================================================== # DBMovie_InitActorDb: Initialize actor.db if missing or empty #------------------------------------------------------------------------------ # Description: # Ensures that the actor metadata cache file (`actor.db`) exists and contains # the appropriate header. If the file does not exist or is empty, it will be # created with the following pipe-separated column headers: # # IMDB_ID|TMDB_ID|NAME|DOB|DOD|PHOTO_URL|BLOGGER_URL # # This file is used to cache actor metadata retrieved from TMDb and Blogger. # # Globals Used: # ACTOR_DB_PATH - Full path to the actor.db file # DEBUG - If set, logs the creation of the file # # Outputs: # - Creates actor.db with header if the file is missing or empty # - Logs a debug message if DEBUG is enabled # # Return: # 0 - Always #============================================================================== DBMovie_InitActorDb() { mkdir -p "$(dirname "${ACTOR_DB_PATH}")" if [[ ! -s "${ACTOR_DB_PATH}" ]]; then echo "IMDB_ID|TMDB_ID|NAME|DOB|DOD|PHOTO_URL|BLOGGER_URL" > "${ACTOR_DB_PATH}" (( DEBUG )) && EchoD "[DBMovie_InitActorDb] Created ${ACTOR_DB_PATH} with header" fi } #============================================================================== # DBMovie_GetActorDbPath: Return the absolute path to the actor.db cache #------------------------------------------------------------------------------ # Description: # Returns the full path to the actor database file used for storing cached # metadata about actors. This includes fields like IMDb ID, TMDb ID, name, # birth/death dates, photo URL, and Blogger post URL. # # Globals Used: # ACTOR_DB_PATH - Path to the actor.db file (typically under ${HOME}/.MyBlogCache) # # Outputs: # Echoes the actor.db path to stdout # # Return: # 0 - Always #============================================================================== DBMovie_GetActorDbPath() { echo "${ACTOR_DB_PATH}" } #============================================================================== # DBMovie_LoadActorRecord: Load the full actor record from actor.db #------------------------------------------------------------------------------ # Description: # Retrieves a single actor's metadata entry from the local actor.db file # using their IMDb ID as the key. The actor.db contains one tab-separated # line per actor, with fields in the following order: # # IMDb_ID, TMDb_ID, Name, DOB, DOD, Photo_URL, Blogger_URL # # If no match is found, the function returns a non-zero status code. # # Arguments: # ${1} - IMDb ID (e.g., nm0000493) # # Globals Used: # ACTOR_DB_PATH - Resolved via DBMovie_GetActorDbPath # # Outputs: # Prints a tab-separated actor record if found: # <IMDb_ID>\t<TMDb_ID>\t<Name>\t<DOB>\t<DOD>\t<Photo_URL>\t<Blogger_URL> # # Return: # 0 - If the actor record is found # 1 - If no matching record is found #============================================================================== DBMovie_LoadActorRecord() { local -r imdb_id="$1" local -r file="$(DBMovie_GetActorDbPath)" grep -P "^${imdb_id}\t" "${file}" || return 1 } #============================================================================== # DBMovie_SaveActorRecord: Adds or updates an actor entry in actor.db #------------------------------------------------------------------------------ # Description: # Adds a new actor record or updates an existing one in the local actor.db # metadata cache. If the IMDb ID already exists, the entry is replaced. # Fields are tab-separated and written in the following order: # # IMDb_ID, TMDb_ID, Name, DOB, DOD, Photo_URL, Blogger_URL # # Values are sanitized to strip tabs and linebreaks before writing. # # Usage: # DBMovie_SaveActorRecord <IMDb_ID> <TMDb_ID> <Name> <DOB> <DOD> <PhotoURL> <BlogURL> # # Arguments: # ${1} - IMDb ID (e.g., nm0000493) # ${2} - TMDb ID (e.g., 12345) # ${3} - Actor name (e.g., Jack Lemmon) # ${4} - Date of birth (YYYY-MM-DD) # ${5} - Date of death (YYYY-MM-DD or empty) # ${6} - Photo URL (full TMDb image path) # ${7} - Blogger post URL (or empty string) # # Globals Used: # ACTOR_DB_PATH - Retrieved via DBMovie_GetActorDbPath # # Outputs: # - Writes updated actor.db file to disk # # Return: # 0 - Always #============================================================================== DBMovie_SaveActorRecord() { local imdb_id="$1" local tmdb_id="$2" local name="$3" local dob="$4" local dod="$5" local photo_url="$6" local blog_url="$7" local -r file="$(DBMovie_GetActorDbPath)" # Sanitize name="${name//[$'\t\r\n']}" photo_url="${photo_url//[$'\t\r\n']}" blog_url="${blog_url//[$'\t\r\n']}" # Remove existing entry and add new one grep -v -P "^${imdb_id}\t" "${file}" > "${file}.tmp" 2>/dev/null || true printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "${imdb_id}" "${tmdb_id}" "${name}" "${dob}" "${dod}" "${photo_url}" "${blog_url}" >> "${file}.tmp" mv "${file}.tmp" "${file}" } #============================================================================== # DBMovie_UpdateActorBloggerUrl: Update the Blogger URL field in actor.db #------------------------------------------------------------------------------ # Description: # Loads the full actor record from actor.db using the provided IMDb ID, # replaces the Blogger URL field with the given new value, and writes the # updated record back to the actor.db file. # # All other metadata fields (TMDb ID, name, DOB, etc.) are preserved. # # Arguments: # ${1} - IMDb ID (e.g., nm0000493) # ${2} - New Blogger URL path suffix (e.g., 2025/03/actor-jack-lemmon-1925-2001) # # Globals Used: # ACTOR_DB_PATH - Resolved internally via DBMovie_LoadActorRecord and SaveActorRecord # # Outputs: # - Updates the actor.db file with the new Blogger URL # # Return: # 0 - Success # 1 - If the actor record is not found #============================================================================== DBMovie_UpdateActorBloggerUrl() { local -r imdb_id="$1" local -r new_url="$2" local record record=$(DBMovie_LoadActorRecord "${imdb_id}") || return 1 IFS=$'\t' read -r -a fields <<< "${record}" DBMovie_SaveActorRecord "${fields[0]}" "${fields[1]}" "${fields[2]}" "${fields[3]}" "${fields[4]}" "${fields[5]}" "${new_url}" } #============================================================================== # DBMovie_UpdateActorPhotoUrl: Update the actor's photo URL in actor.db #------------------------------------------------------------------------------ # Description: # Retrieves an actor record from actor.db using the provided IMDb ID, # updates the photo URL field with a new value, and rewrites the modified # record back to the actor metadata file. All other fields remain unchanged. # # Arguments: # ${1} - IMDb ID (e.g., nm0000493) # ${2} - New photo URL (full TMDb path or other image link) # # Globals Used: # ACTOR_DB_PATH - Used indirectly via DBMovie_LoadActorRecord and SaveActorRecord # # Outputs: # - Modifies the actor.db file with the new photo URL # # Return: # 0 - Success # 1 - If no record is found for the given IMDb ID #============================================================================== DBMovie_UpdateActorPhotoUrl() { local -r imdb_id="$1" local -r new_url="$2" local record record=$(DBMovie_LoadActorRecord "${imdb_id}") || return 1 IFS=$'\t' read -r -a fields <<< "${record}" DBMovie_SaveActorRecord "${fields[0]}" "${fields[1]}" "${fields[2]}" "${fields[3]}" "${fields[4]}" "${new_url}" "${fields[6]}" } #============================================================================== # DBMovie_SaveActorMetadata: Complete overwrite of actor record in actor.db #------------------------------------------------------------------------------ # Description: # Performs a full overwrite of an actor's metadata entry in actor.db using # the provided fields. Any existing record for the given IMDb ID is removed # and replaced. This function serves as a parallel to DBMovie_SaveActorRecord # but operates independently of record-loading helpers. # # Fields are stored as a tab-separated line in this order: # IMDb_ID, TMDb_ID, Name, DOB, DOD, Photo_URL, Blogger_URL # # Arguments: # ${1} - IMDb ID (e.g., nm0000493) # ${2} - TMDb ID (e.g., 12345) # ${3} - Actor name (e.g., Sylvester Stallone) # ${4} - Date of birth (YYYY-MM-DD) # ${5} - Date of death (optional, may be empty) # ${6} - Photo URL (full image path from TMDb or other source) # ${7} - Blogger post URL (may be empty) # # Globals Used: # MY_MDB_CACHE - Root path for metadata caching # ACTOR_DB_PATH - Full path to actor.db # # Outputs: # - Writes or replaces a single line in actor.db with new metadata # # Return: # 0 - Always #============================================================================== DBMovie_SaveActorMetadata() { local imdb_id="${1}" local tmdb_id="${2}" local name="${3}" local dob="${4}" local dod="${5}" local photo_url="${6}" local blog_url="${7}" mkdir -p "${MY_MDB_CACHE}" local cache_file="${ACTOR_DB_PATH}" # Remove any previous line and insert new if [[ -f "${cache_file}" ]]; then grep -v -P "^${imdb_id}\t" "${cache_file}" > "${cache_file}.tmp" || true mv "${cache_file}.tmp" "${cache_file}" fi echo -e "${imdb_id}\t${tmdb_id}\t${name}\t${dob}\t${dod}\t${photo_url}\t${blog_url}" >> "${cache_file}" } #============================================================================== # DBMovie_BuildActorFilmographyTable: Build HTML table rows for actor filmography #------------------------------------------------------------------------------ # Description: # Generates the HTML table rows for an actor's filmography by retrieving # movie credits from TMDb using the person/<id>/movie_credits endpoint. For # each movie role, it computes the actor's age at release, checks whether # the movie was released after the actor's death (marks ❌), and builds a # complete <tr> element with: # - Release year # - Title # - Role name # - Calculated age # - Link to IMDb search for the title # # The generated HTML table is returned as a single string via stdout. # # Arguments: # ${1} - TMDb actor ID (e.g., 12345) # # Globals Used: # ACTOR_TMDB_ID - TMDb actor ID (reference only) # ACTOR_BIRTH_YEAR - Year of birth (YYYY) # ACTOR_DOD - Date of death (YYYY-MM-DD or empty) # TMDB_API_KEY - Required TMDb API key # MOVIE_DB_PATH - Path to local movie metadata file (not used here but assumed related) # # Outputs: # Echoes a complete HTML <table> element with one row per film # # Return: # 0 - Always #============================================================================== DBMovie_BuildActorFilmographyTable() { local tmdb_id="${1}" local output="" local tmpfile tmpfile=$(mktemp) local credits_url="https://api.themoviedb.org/3/person/${tmdb_id}/movie_credits?api_key=${TMDB_API_KEY}" curl --silent --fail "${credits_url}" > "${tmpfile}" output+="<h2>Filmography</h2>" output+="<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">" output+="<tr><th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th></tr>" jq -r '.cast[] | select(.release_date != null and .title != null) | [.release_date, .title, .character] | @tsv' "${tmpfile}" | while IFS=$'\t' read -r release title role; do year="${release:0:4}" age="?" if [[ "${ACTOR_BIRTH_YEAR}" =~ ^[0-9]+$ && "${year}" =~ ^[0-9]+$ ]]; then age=$((year - ACTOR_BIRTH_YEAR)) fi if [[ -n "${ACTOR_DOD}" && "${year}" -gt "${ACTOR_DOD:0:4}" ]]; then age='<span style="color:red;">❌</span>' fi local search_url="https://www.imdb.com/find?q=$(urlencode "${title} ${year}")" output+="<tr><td>${year}</td><td>${title}</td><td>${role}</td><td>${age}</td><td><a href=\"${search_url}\" target=\"_blank\">🔍</a></td></tr>" done output+="</table>" rm -f "${tmpfile}" echo "${output}" } #============================================================================== # DBMovie_GetActorBloggerUrl: Return the Blogger URL for an actor by IMDb ID #------------------------------------------------------------------------------ # Description: # Looks up the actor.db file and retrieves the Blogger URL associated with the # specified IMDb ID. The actor.db is a tab-separated file where the 7th field # stores the Blogger URL. If no match is found, the function returns nothing. # # Arguments: # ${1} - IMDb ID of the actor (e.g., nm0000493) # # Globals Used: # ACTOR_DB_PATH - Path to the actor.db file containing cached actor metadata # # Outputs: # - Echoes the Blogger URL to stdout if found # - Returns silently if no match is found or actor.db is missing # # Return: # 0 - If the Blogger URL was found and printed # 1 - If the IMDb ID is missing or no record exists #============================================================================== DBMovie_GetActorBloggerUrl() { local -r imdb_id="${1}" [[ -z "${imdb_id}" ]] && return 1 [[ ! -f "${ACTOR_DB_PATH}" ]] && return 1 awk -F'\t' -v id="${imdb_id}" 'NR > 1 && $1 == id { print $7 }' "${ACTOR_DB_PATH}" } #============================================================================== # DBMovie_InitMovieCreditsDb: Initialize movie_credits.db if missing or empty #------------------------------------------------------------------------------ # Description: # Ensures that the movie_credits.db file exists and contains the appropriate # header. If the file is missing or empty, it is created and the header line # is written. The expected format is a pipe-separated file: # # MOVIE_ID|ACTOR_ID|ROLE # # This file is used to cache actor-to-movie credit mappings retrieved from TMDb. # # Globals Used: # MOVIE_CREDITS_DB_PATH - Path to the movie credits cache file # DEBUG - If enabled, logs a debug message when file is created # # Outputs: # - Creates movie_credits.db with a header if not present # - Logs debug message if DEBUG is enabled # # Return: # 0 - Always #============================================================================== DBMovie_InitMovieCreditsDb() { mkdir -p "$(dirname "${MOVIE_CREDITS_DB_PATH}")" if [[ ! -s "${MOVIE_CREDITS_DB_PATH}" ]]; then echo "MOVIE_ID|ACTOR_ID|ROLE" > "${MOVIE_CREDITS_DB_PATH}" (( DEBUG )) && EchoD "[DBMovie_InitMovieCreditsDb] Created ${MOVIE_CREDITS_DB_PATH} with header" fi } #============================================================================== # DBMovie_GetMovieCreditsDbPath: Return the full path to movie_credits.db #------------------------------------------------------------------------------ # Description: # Echoes the absolute path to the movie credits database file, which is used # to store mappings between actor IDs and their movie roles. The file format # is pipe-separated (|) and includes the following fields: # # MOVIE_ID|ACTOR_ID|ROLE # # This function provides a central reference for consistent path usage. # # Globals Used: # MOVIE_CREDITS_DB_PATH - Path to the movie credits database file # # Outputs: # Echoes the path to stdout # # Return: # 0 - Always #============================================================================== DBMovie_GetMovieCreditsDbPath() { echo "${MOVIE_CREDITS_DB_PATH}" } #============================================================================== # DBMovie_SaveMovieCredit: Append a movie credit record to movie_credits.db #------------------------------------------------------------------------------ # Description: # Saves a new entry to the movie_credits.db file associating a movie ID with # an actor ID and their role in that film. Entries are tab-separated and stored # in the format: # # MOVIE_ID<TAB>ACTOR_ID<TAB>ROLE # # This function does not enforce uniqueness or deduplication. It is the caller's # responsibility to avoid inserting duplicate records for the same (movie_id, actor_id) pair. # # Usage: # DBMovie_SaveMovieCredit <movie_id> <actor_id> <role> # # Arguments: # ${1} - TMDb movie ID (e.g., 550) # ${2} - TMDb actor ID (e.g., 819) # ${3} - Character name / role (e.g., "Rocky Balboa") # # Globals Used: # MOVIE_CREDITS_DB_PATH - Path to movie credits cache file # # Outputs: # - Appends a single line to movie_credits.db # # Return: # 0 - Always #============================================================================== DBMovie_SaveMovieCredit() { local movie_id="${1}" local actor_id="${2}" local role="${3}" local -r file="${MOVIE_CREDITS_DB_PATH}" role="${role//[$'\t\r\n']}" # Sanitize role echo -e "${movie_id}\t${actor_id}\t${role}" >> "${file}" } #============================================================================== # DBMovie_LoadMovieCreditsByActor: Load all movie credits for a given TMDb actor ID #------------------------------------------------------------------------------ # Description: # Retrieves all movie credit entries from movie_credits.db for the specified # TMDb actor ID. Each entry in the file is tab-separated and represents a # single role in a movie, with fields: # # MOVIE_ID<TAB>ACTOR_ID<TAB>ROLE # # This function filters the database and returns only those lines where the # actor ID matches. It returns a non-zero status code if no records are found. # # Arguments: # ${1} - TMDb actor ID (e.g., 819) # # Globals Used: # MOVIE_CREDITS_DB_PATH - Path to the movie credits database # # Outputs: # - Echoes matching tab-separated lines to stdout: # <MOVIE_ID>\t<ACTOR_ID>\t<ROLE> # # Return: # 0 - If one or more matching credits are found # 1 - If no matching credits exist or the file is missing #============================================================================== DBMovie_LoadMovieCreditsByActor() { local -r actor_id="${1}" local -r file="${MOVIE_CREDITS_DB_PATH}" grep -P "^\d+\t${actor_id}\t" "${file}" || return 1 } #============================================================================== # DBMovie_DeleteCreditsByActor: Remove all movie credit entries for a given actor #------------------------------------------------------------------------------ # Description: # Deletes all entries in movie_credits.db that match the specified TMDb actor ID. # This function is useful for purging outdated or erroneous credit records. # The operation is performed by filtering all lines that do not match the actor ID # and writing the result back to the original file. # # Arguments: # ${1} - TMDb actor ID (e.g., 819) # # Globals Used: # MOVIE_CREDITS_DB_PATH - Path to the movie credits database # # Outputs: # - Overwrites movie_credits.db with all non-matching entries # # Return: # 0 - Always (even if no matching entries were found) #============================================================================== DBMovie_DeleteCreditsByActor() { local -r actor_id="${1}" local -r file="${MOVIE_CREDITS_DB_PATH}" grep -v -P "^\d+\t${actor_id}\t" "${file}" > "${file}.tmp" || true mv "${file}.tmp" "${file}" } #============================================================================== # DBMovie_InitMovieDb: Initialize movie.db if missing or empty #------------------------------------------------------------------------------ # Description: # Ensures that the movie metadata cache file (`movie.db`) exists and contains # the appropriate header line. If the file is missing or empty, it is created # with the following pipe-separated column headers: # # TMDB_ID|IMDB_ID|COUNTRY|TITLE_EN|TITLE_ORIG|RELEASE_DATE # # This file is used to cache metadata for movies retrieved from TMDb. # # Globals Used: # MY_MDB_CACHE - Directory in which the cache file is stored # MOVIE_DB_PATH - Full path to the movie.db file # DEBUG - If enabled, logs file creation # # Outputs: # - Creates movie.db with a header line if the file is missing or empty # - Logs a debug message on creation if DEBUG is set # # Return: # 0 - Always #============================================================================== DBMovie_InitMovieDb() { mkdir -p "${MY_MDB_CACHE}" if [[ ! -s "${MOVIE_DB_PATH}" ]]; then echo "TMDB_ID|IMDB_ID|COUNTRY|TITLE_EN|TITLE_ORIG|RELEASE_DATE" > "${MOVIE_DB_PATH}" (( DEBUG )) && EchoD "[DBMovie_InitMovieDb] Created ${MOVIE_DB_PATH} with header" fi } #============================================================================== # DBMovie_SaveMovieRecord: Overwrite or append a movie record in movie.db #------------------------------------------------------------------------------ # Description: # Inserts or updates a movie record in the local movie metadata cache file # (movie.db). If an existing entry with the same TMDb ID is found, it is removed. # The function writes a sanitized pipe-separated line containing the following fields: # # TMDB_ID|IMDB_ID|COUNTRY|TITLE_EN|TITLE_ORIG|RELEASE_DATE # # This function does not handle the watched date field. It is intended for # storing static metadata only. # # Arguments: # ${1} - TMDb movie ID (e.g., 550) # ${2} - IMDb ID (e.g., tt0137523) # ${3} - ISO 3166-1 alpha-2 country code (e.g., US) # ${4} - English title # ${5} - Original title # ${6} - Release date (YYYY-MM-DD) # # Globals Used: # MOVIE_DB_PATH - Path to the movie.db metadata cache # # Outputs: # - Rewrites movie.db with the updated or new entry # # Return: # 0 - Always #============================================================================== DBMovie_SaveMovieRecord() { local -r tmdb_id="${1}" local -r imdb_id="${2}" local -r country="${3}" local title_en="${4}" local title_orig="${5}" local -r release_date="${6}" # Sanitize input (remove tabs, newlines, pipes) title_en="${title_en//[$'\t\r\n|']}" title_orig="${title_orig//[$'\t\r\n|']}" local -r clean_country="${country//[$'\t\r\n|']}" local -r clean_release="${release_date//[$'\t\r\n|']}" # Remove any existing entry for this TMDb ID grep -v -P "^${tmdb_id}\|" "${MOVIE_DB_PATH}" > "${MOVIE_DB_PATH}.tmp" 2>/dev/null || true # Add new entry echo "${tmdb_id}|${imdb_id}|${clean_country}|${title_en}|${title_orig}|${clean_release}" >> "${MOVIE_DB_PATH}.tmp" # Replace file mv "${MOVIE_DB_PATH}.tmp" "${MOVIE_DB_PATH}" } #============================================================================== # DBMovie_SaveMovieMetadata: Save or update a movie entry in movie.db #------------------------------------------------------------------------------ # Description: # Adds or updates a movie record in ${MOVIE_DB_PATH}. If a record with the # same TMDb ID exists, it is overwritten. The final line contains: # # TMDB_ID | IMDB_ID | COUNTRY | ENGLISH_TITLE | ORIGINAL_TITLE | # RELEASE_DATE | WATCHED_DATE | BLOGGER_URL | PHOTO_URL # # BLOGGER_URL is stored without the domain prefix (e.g., just "2025/03/..."). # PHOTO_URL is stored in full. # # Globals: # MOVIE_DB_PATH - Path to ~/.MyMDBCache/movie.db # # Arguments: # ${1} - TMDb ID # ${2} - IMDb ID # ${3} - Country (ISO 2-letter code) # ${4} - English title # ${5} - Original title # ${6} - Release date (YYYY-MM-DD) # ${7} - Watched date (optional, may be empty) # ${8} - Blogger URL (full, will be trimmed) # ${9} - Photo URL (optional, full) # # Outputs: # Writes to ${MOVIE_DB_PATH} # Logs debug info via EchoD #============================================================================== DBMovie_SaveMovieMetadata() { local -r tmdb_id="${1}" local -r imdb_id="${2}" local -r country="${3}" local -r english_title="${4}" local -r original_title="${5}" local -r release_date="${6}" local -r watched_date="${7}" local -r full_blogger_url="${8}" local -r photo_url="${9}" local -r movie_file="${MOVIE_DB_PATH}" local blogger_suffix="${full_blogger_url}" local -r domain_prefix="https://afberendsen.blogspot.com/" # Trim the Blogger domain prefix if [[ "${blogger_suffix}" == "${domain_prefix}"* ]]; then blogger_suffix="${blogger_suffix#${domain_prefix}}" fi mkdir -p "$(dirname "${movie_file}")" touch "${movie_file}" # Remove old record, if exists grep -v "^${tmdb_id}|" "${movie_file}" > "${movie_file}.tmp" || true # Append new line local -r SEP='|' echo "${tmdb_id}${SEP}${imdb_id}${SEP}${country}${SEP}${english_title}${SEP}${original_title}${SEP}${release_date}${SEP}${watched_date}${SEP}${blogger_suffix}${SEP}${photo_url}" >> "${movie_file}.tmp" mv "${movie_file}.tmp" "${movie_file}" EchoD "${FUNCNAME[0]}:${LINENO}:✅ Saved movie metadata for TMDb ID ${tmdb_id}" } #============================================================================== # DBMovie_HasMovieMetadata - Checks if a movie (by TMDb ID) exists in movie.db # Usage : DBMovie_HasMovieMetadata <tmdb_movie_id> # Returns : 0 if found, 1 otherwise #============================================================================== DBMovie_HasMovieMetadata() { local -r tmdb_id="$1" grep -q -P "^${tmdb_id}\t" "${MOVIE_DB_PATH}" 2>/dev/null } #============================================================================== # DBMovie_DeduplicateMovieDb - Cleans up duplicate TMDb or IMDb entries in movie.db # Keeps only the last occurrence for any TMDb or IMDb ID #============================================================================== DBMovie_DeduplicateMovieDb() { local -r file="${MOVIE_DB_PATH}" local -r tmpfile="${file}.dedup.tmp" if [[ ! -f "${file}" ]]; then EchoE "❌ [DBMovie_DeduplicateMovieDb] Movie DB file not found: ${file}" return 1 fi Echo "🧹 Deduplicating movie.db at ${file}..." # Preserve header head -n 1 "${file}" > "${tmpfile}" # Deduplicate by keeping only the last occurrence of TMDb or IMDb ID tail -n +2 "${file}" | tac | awk -F'\t' ' { tmdb = $1; imdb = $2; if (!(tmdb in seen_tmdb) && !(imdb in seen_imdb)) { lines[NR] = $0; seen_tmdb[tmdb] = 1; seen_imdb[imdb] = 1; } } END { for (i = NR; i >= 1; i--) { if (i in lines) print lines[i]; } } ' >> "${tmpfile}" mv "${tmpfile}" "${file}" Echo "✅ movie.db has been deduplicated successfully." } #============================================================================== # DBMovie_GetMovieWatchedDate: Retrieve watch date from movie.db for a given TMDb ID #------------------------------------------------------------------------------ # Description: # Looks up the watch date field in movie.db based on TMDb movie ID. # Assumes the watch date is in column 7. If the movie or the field is missing, # an empty string is returned. # # Globals Used: # MOVIE_DB_PATH - Path to movie.db # # Arguments: # $1 - TMDb movie ID # # Outputs: # Echoes the watched date (YYYY-MM-DD) or nothing if not found. #============================================================================== DBMovie_GetMovieWatchedDate() { local -r movie_id="${1}" grep -P "^${movie_id}\t" "${MOVIE_DB_PATH}" | awk -F '\t' '{ print $7 }' } #============================================================================== # MarkMovieAsWatched: Update WATCHED_DATE field in movie.db for a given TMDb ID #------------------------------------------------------------------------------ # Description: # Updates the watch date field for a specific movie in the movie.db file. If # the movie exists, it replaces the 7th column with the given date (or today # if not specified). Preserves all other fields. A temporary file is used to # ensure atomic writes. # # Globals Used: # MOVIE_DB_PATH - Path to movie.db file # # Arguments: # $1 - TMDb movie ID # $2 - [optional] Watched date (YYYY-MM-DD). If omitted, uses today's date. # # Outputs: # Updates movie.db in place. #============================================================================== MarkMovieAsWatched() { local -r movie_id="${1}" local -r date="${2:-$(date +%F)}" awk -F '\t' -v id="${movie_id}" -v d="${date}" 'BEGIN {OFS=FS} { if ($1 == id) $7 = d; print }' "${MOVIE_DB_PATH}" > "${MOVIE_DB_PATH}.tmp" && mv "${MOVIE_DB_PATH}.tmp" "${MOVIE_DB_PATH}" } #============================================================================== # DBMovie_LoadMoviesByActor: Return movie.db entries by TMDb actor ID #------------------------------------------------------------------------------ # Description: # Outputs all movie.db lines corresponding to a given TMDb actor ID by # looking up matching entries from movie_credits.db. This ensures tab-aligned, # complete movie.db lines are returned (including new fields like blogger_url). # # Globals: # MOVIE_DB_PATH # MOVIE_CREDITS_DB_PATH # # Arguments: # $1 - TMDb actor ID (required) # # Outputs: # Full lines from movie.db (tab-separated) for the given actor. # #============================================================================== DBMovie_LoadMoviesByActor() { (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: $*" local -r actor_tmdb_id="${1}" if [[ -z "${actor_tmdb_id}" ]]; then EchoE "${FUNCNAME[0]}:${LINENO}: Missing TMDb actor ID" return 1 elif [[ ! -s "${MOVIE_CREDITS_DB_PATH}" || ! -s "${MOVIE_DB_PATH}" ]]; then EchoE "${FUNCNAME[0]}:${LINENO}: Required DB file missing" return 1 fi # Extract movie IDs once local tmp_ids tmp_ids="$(mktemp)" awk -F'\t' -v id="${actor_tmdb_id}" '$2 == id { print $1 }' "${MOVIE_CREDITS_DB_PATH}" > "${tmp_ids}" if [[ ! -s "${tmp_ids}" ]]; then EchoW "${FUNCNAME[0]}:${LINENO}: No movie credits found for actor ID ${actor_tmdb_id}" rm -f "${tmp_ids}" return 0 fi # Match full lines in movie.db once grep -F -f "${tmp_ids}" "${MOVIE_DB_PATH}" | grep -E '^[0-9]+\|' rm -f "${tmp_ids}" } #============================================================================== # DBMovie_GetMovieRole: Retrieve actor's role for a given movie from cache #------------------------------------------------------------------------------ # Description: # Extracts the role name an actor played in a specific movie, based on the # combination of TMDb actor ID and TMDb movie ID. The function searches # movie_credits.db and returns the role string. If no role is found, it # returns an empty string. # # Globals: # MY_MDB_CACHE - Base path to local metadata cache directory # FUNCNAME - Used in logging/debug output # # Arguments: # ${1} - TMDb Movie ID (e.g., 239) # ${2} - TMDb Actor ID (e.g., 3151) # # Returns: # Echoes role string on FD 5 (e.g., "Joe / Josephine / Junior") # Writes debug message if role is found #============================================================================== DBMovie_GetMovieRole() { local -r movie_id="${1}" local -r actor_id="${2}" local -r credits_file="${MY_MDB_CACHE}/movie_credits.db" local line role if [[ ! -s "${credits_file}" ]]; then EchoD "${FUNCNAME[0]}:${LINENO}: movie_credits.db is missing or empty" return 1 fi # Match line by both movie ID and actor ID while IFS=$'\t' read -r m_id a_id r; do if [[ "${m_id}" == "${movie_id}" && "${a_id}" == "${actor_id}" ]]; then role="${r}" break fi done < "${credits_file}" if [[ -n "${role}" ]]; then (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Found role: '${role}' for movie_id=${movie_id}, actor_id=${actor_id}" echo "${role}" else echo "" fi } #*************************************************************************************** #*************************************************************************************** #*************************************************************************************** #*************************************************************************************** #============================================================================== # EnsureDependency: Ensure tool is available via install, fallback, or error #------------------------------------------------------------------------------ # Description: # Attempts to locate a required CLI tool. If not found, attempts to install # it based on the current OS: # - Cygwin: runs local setup-x86_64.exe with -P <package> # - Debian/Ubuntu: uses apt-get # - macOS: uses brew # # If install fails or is unsupported, falls back to a named function if given. # # Arguments: # $1 - Tool name (e.g. "jq") # $2 - Package name (same as tool or alternative) # $3 - Fallback function name (optional) # # Globals: # DEBUG (controls debug output) # Outputs: # - Echo status messages # - Defines fallback function if install fails and fallback is provided #============================================================================== EnsureDependency() { local -r tool="${1}" local -r package="${2}" local -r fallback_func="${3:-}" if command -v "${tool}" &>/dev/null; then (( DEBUG )) && EchoD "[EnsureDependency] '${tool}' found" return 0 fi EchoW "Missing tool: '${tool}'. Attempting to install package: ${package}..." case "$(uname -s)" in CYGWIN*) local installer="/setup-x86_64.exe" [[ -x "${installer}" ]] || installer="/cygdrive/f/Downloads/setup-x86_64_2.932.exe" if [[ -x "${installer}" ]]; then "${installer}" -q -P "${package}" &>/dev/null else EchoW "Cygwin installer not found. Please install '${package}' manually." fi ;; Linux) if command -v apt-get &>/dev/null; then sudo apt-get update && sudo apt-get install -y "${package}" fi ;; Darwin) if command -v brew &>/dev/null; then brew install "${package}" fi ;; *) EchoW "⚠️ Unsupported platform. Cannot install '${package}' automatically." ;; esac if command -v "${tool}" &>/dev/null; then Echo "✔ Tool '${tool}' is now available." return 0 elif [[ -n "${fallback_func}" ]]; then EchoW "Tool '${tool}' still missing. Attempting fallback: '${fallback_func}'" "${fallback_func}" return $? else ExitWithMsg "❌ Required tool '${tool}' could not be installed." fi } #============================================================================== # DefineUrlencodeFallback: Define a fallback implementation for 'urlencode' #------------------------------------------------------------------------------ # Description: # Creates a shell function named `urlencode` to encode a string for safe use # in URLs if it is not already defined. This fallback is only used when a # native or installed `urlencode` command is unavailable. # # Priority is given to the following implementations: # 1. Python 3 (urllib.parse.quote) # 2. Perl (URI::Escape) # 3. Pure Bash fallback using percent encoding # # This function should be called during dependency checks when # --check-deps-local or --check-deps-install is active. # # Globals Used: # DEBUG - If set, logs debug output on existing function # # Outputs: # - Defines a shell function named `urlencode` # - Logs which method was used for fallback definition # # Return: # 0 - Always #============================================================================== DefineUrlencodeFallback() { if declare -F urlencode &>/dev/null; then (( DEBUG )) && EchoD "'urlencode' already defined, skipping fallback setup" elif command -v python3 &>/dev/null; then urlencode() { python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$1" } EchoW "Defined fallback 'urlencode' using python3" elif command -v perl &>/dev/null; then urlencode() { perl -MURI::Escape -e 'print uri_escape($ARGV[0]);' "$1" } EchoW "Defined fallback 'urlencode' using perl" else urlencode() { local LANG=C local string="${1}" local length="${#string}" local i for (( i = 0; i < length; i++ )); do local c="${string:$i:1}" case "${c}" in [a-zA-Z0-9.~_-]) printf "%s" "${c}" ;; *) printf "%%%02X" "'${c}" ;; esac done } EchoW "Defined fallback 'urlencode' using pure bash" fi return 0 } #============================================================================== # GetBlogAccessToken: Retrieve a Blogger API access token using a refresh token #------------------------------------------------------------------------------ # Description: # Obtains a new access token from the Google OAuth 2.0 endpoint using the # configured refresh token. This function retries every few seconds until # a valid token is obtained and exported. # # It uses the `curl` command to send a POST request and parses the JSON # response to extract `.access_token` using `jq`. If the token is invalid # or the request fails, the function sleeps for a fixed interval and retries. # # Globals Used: # GOOGLE_CLIENT_ID - OAuth 2.0 client ID # GOOGLE_CLIENT_SECRET - OAuth 2.0 client secret # GOOGLE_REFRESH_TOKEN - Refresh token obtained via initial authorization # DEBUG - If set, logs additional diagnostics # # Globals Set: # BLOG_ACCESS_TOKEN - The newly retrieved Blogger API access token (exported) # # Outputs: # - Echoes status messages and errors to the console # - Logs detailed retry messages if the request fails # # Return: # 0 - Once a valid access token is retrieved #============================================================================== GetBlogAccessToken() { Echo "Obtaining Blog Access Token..." declare -g BLOG_ACCESS_TOKEN="" local -i attempt=0 local -r MAX_WAIT=2 while :; do (( attempt++ )) local BLOG_TOKEN_RESPONSE BLOG_TOKEN_RESPONSE=$(curl --silent --request POST \ --data "client_id=${GOOGLE_CLIENT_ID}" \ --data "client_secret=${GOOGLE_CLIENT_SECRET}" \ --data "refresh_token=${GOOGLE_REFRESH_TOKEN}" \ --data "grant_type=refresh_token" \ https://oauth2.googleapis.com/token) BLOG_ACCESS_TOKEN=$(jq -r '.access_token' <<<"${BLOG_TOKEN_RESPONSE}") if [[ -n "${BLOG_ACCESS_TOKEN}" && "${BLOG_ACCESS_TOKEN}" != "null" ]]; then (( DEBUG )) && EchoD "Access token retrieved on attempt #${attempt}" break fi EchoE "Failed to retrieve a valid access token. Retrying in ${MAX_WAIT}s (attempt #${attempt})..." sleep ${MAX_WAIT} done (( DEBUG )) && EchoD "Token is '${BLOG_ACCESS_TOKEN}'" } #============================================================================== # ValidateTmdbKey: Check if the configured TMDb API key is valid #------------------------------------------------------------------------------ # Description: # Sends a test request to the TMDb API to validate the API key configured # in the global variable `TMDB_API_KEY`. The request uses a known IMDb ID # (`tt0030062`) to perform a simple lookup. # # If the response contains a `.status_code` field, this typically indicates # an error such as invalid API key or unauthorized access. In that case, # a warning is shown and `TMDB_KEY_VALID` is set to 0. Otherwise, the key # is assumed to be valid, and `TMDB_KEY_VALID` is set to 1. # # Globals Used: # TMDB_API_KEY - The TMDb API key to be tested # DEBUG - If set, logs confirmation of validity # # Globals Set: # TMDB_KEY_VALID - 1 if key is valid, 0 if invalid # # Outputs: # - Echoes a warning if the API key appears to be invalid # - Logs validation status if DEBUG is enabled # # Return: # 0 - Always #============================================================================== ValidateTmdbKey() { local response response=$(curl --silent --get "https://api.themoviedb.org/3/find/tt0030062" \ --data-urlencode "api_key=${TMDB_API_KEY}" \ --data-urlencode "external_source=imdb_id") if jq -e '.status_code' <<<"${response}" &>/dev/null; then Echo "WARNING=> ⚠️ TMDb API key appears invalid. Actor data will be skipped unless --force-actors is used." TMDB_KEY_VALID=0 else (( DEBUG )) && EchoD "TMDb API key is valid." TMDB_KEY_VALID=1 fi } #============================================================================== # ShowHelp: Display detailed usage help for this script #------------------------------------------------------------------------------ # Globals: # SCRIPT_VERSION - Script version (used in --version output) # Outputs: # Prints all supported options, modes, and examples to stdout #============================================================================== ShowHelp() { cat <<EOF Usage: ${0##*/} [OPTIONS] Modes: --actor NAME Generate a filmography post for the specified actor --imdb ID Use IMDb ID directly instead of actor name (e.g., nm0000493) --movie-list Generate an update post based on modified movie folders Actor Cache Utility: --show-blogger-url Print Blogger URL for actor in cache --delete-blogger-url Delete Blogger URL for actor in cache --update-blogger-url URL Update Blogger URL for actor in cache --revert-blogger-url [N] Revert Blogger URL to previous state (default N=1) --history-blogger-url Show full Blogger URL change history Dependency Check Modes (mutually exclusive): --check-deps Check for required tools; exit if any are missing --check-deps-local Check and fallback to local functions when possible --check-deps-install Attempt to install missing tools via Cygwin installer General Options: -h, --help Show this help message and exit -V, --version Show script version and exit -v, --verbose Increase verbosity (can be repeated) -D, --debug Enable debug output -n, --dry-run Show what would happen, but make no changes --force Override checks (e.g., update identical Blogger URL) Movie Update Options (used with --movie-list): -d, --date DATE Specify reference date (format: YYYY-MM-DD) -y, --year YEAR Process only the given year -r, --range RANGE Process a year range (e.g., 1930-1939 or -2020 or 1990-) -t, --title TITLE Override default Blogger post title -l, --label LABEL Add a Blogger label (can be repeated) --publish Submit post as LIVE (default is DRAFT) --force Skip confirmation warning when using --publish with year filters --no-html Do not generate HTML in output --force-actors Try to resolve actor info from TMDb even if key is invalid Examples: ${0##*/} --actor "Jack Lemmon" ${0##*/} --imdb nm0000493 ${0##*/} --actor "Jack Lemmon" --update-blogger-url 'https://...' ${0##*/} --actor "Jack Lemmon" --revert-url 2 ${0##*/} --movie-list --date 2025-04-16 --range 1930-1939 --publish ${0##*/} --check-deps-local EOF } #============================================================================== # GetActorMappingPath: Return the path to the legacy actor-to-blog mapping file #------------------------------------------------------------------------------ # Description: # Returns the full path to the file used for mapping IMDb actor IDs to # Blogger post URL suffixes. This was the original format before the adoption # of actor.db. The file is typically located under ~/.MyBlogCache. # # This function is retained for backward compatibility with older scripts or # migration tooling. The preferred method now uses actor.db instead. # # Globals Used: # HOME - The user’s home directory (used to construct the path) # # Outputs: # - Echoes the full file path: ~/.MyBlogCache/actor_posts.map # # Return: # 0 - Always #============================================================================== GetActorMappingPath() { echo "${HOME}/.MyBlogCache/actor_posts.map" } #============================================================================== # LoadActorPostUrl: Retrieve Blogger URL suffix for a given IMDb actor ID #------------------------------------------------------------------------------ # Description: # Looks up the legacy actor-to-blog mapping file (actor_posts.map) to find # the Blogger URL suffix associated with the given IMDb ID. Each line in the # mapping file is expected to be in the format: # # IMDB_ID|blogger-url-suffix # # This function is part of legacy support and is typically replaced by newer # database access via actor.db. # # Arguments: # ${1} - IMDb actor ID (e.g., nm0000493) # # Globals Used: # HOME - Used to determine the mapping file path # # Outputs: # - Echoes the matching Blogger URL suffix (without base domain) to stdout # # Return: # 0 - Success (match found) # 1 - Failure (file missing or no match found) #============================================================================== LoadActorPostUrl() { local -r imdb_id="${1}" local -r file_path="$(GetActorMappingPath)" [[ ! -f "${file_path}" ]] && return 1 grep -E "^${imdb_id}\|" "${file_path}" | cut -d'|' -f2- } #============================================================================== # SaveActorPostUrl: Save or update the Blogger URL suffix for a given IMDb ID #------------------------------------------------------------------------------ # Description: # Writes or updates the mapping between an IMDb actor ID and a Blogger URL # suffix in the legacy actor mapping file `actor_posts.map`. If an existing # entry for the IMDb ID exists, it is removed before appending the new one. # # This function maintains backward compatibility for systems that use the # older `actor_posts.map` format (before the adoption of actor.db). # # Arguments: # ${1} - IMDb ID (e.g., nm0000493) # ${2} - Blogger URL suffix (e.g., 2025/04/actor-jack-lemmon-1925-2001) # # Globals Used: # HOME - Used to derive the mapping file path # # Outputs: # - Overwrites or appends a line in ~/.MyBlogCache/actor_posts.map # # Return: # 0 - Always #============================================================================== SaveActorPostUrl() { local -r imdb_id="${1}" local -r path_suffix="${2}" local -r file_path="$(GetActorMappingPath)" mkdir -p "$(dirname "${file_path}")" # Remove previous entry if any grep -v -E "^${imdb_id}\|" "${file_path}" > "${file_path}.tmp" 2>/dev/null || true echo "${imdb_id}|${path_suffix}" >> "${file_path}.tmp" mv "${file_path}.tmp" "${file_path}" } #============================================================================== # GetActorLifespan: Builds string "Name (YYYY–YYYY)" using parsed metadata #------------------------------------------------------------------------------ # Description: # Constructs a human-readable actor lifespan string using global metadata # variables populated during actor data processing. The format follows: # # Name (YYYY–YYYY) ← if death year is known # Name (YYYY– ) ← if actor is still living or date is unknown # # If either the birth or death year is not available, fallback values are # used ("????" for birth, empty string for death). This string is primarily # used in blog post titles and summaries. # # Globals Used: # ACTOR_NAME - Actor's full name # ACTOR_DOB - Actor's birth date (YYYY-MM-DD or partial) # ACTOR_DOD - Actor's date of death (optional) # DEBUG - If set, prints diagnostics # # Outputs: # Echoes the formatted lifespan string to stdout # # Return: # 0 - Always #============================================================================== GetActorLifespan() { EchoD "Inside GetActorLifespan: ACTOR_NAME=${ACTOR_NAME}, ACTOR_DOB=${ACTOR_DOB}, ACTOR_DOD=${ACTOR_DOD}" if [[ -z "${ACTOR_NAME}" ]]; then EchoW "⚠️ WARNING: GetActorLifespan called before ACTOR_NAME was populated!" fi local name="${ACTOR_NAME}" local birth="${ACTOR_DOB}" local death="${ACTOR_DOD}" # Format year of birth and death local born="${birth:0:4}" local died="${death:0:4}" # Fallback logic [[ -z "${born}" || "${born}" == "null" ]] && born="????" [[ -z "${died}" || "${died}" == "null" ]] && died="" # Compose final string if [[ -n "${died}" ]]; then echo "${name} (${born}–${died})" else echo "${name} (${born}– )" fi } #============================================================================== # GenerateActorFilmography: Top-level wrapper to generate and display actor filmography #------------------------------------------------------------------------------ # Description: # High-level driver that coordinates all steps required to generate the # HTML-formatted actor filmography post. This function: # # - Initializes caches # - Loads or fetches actor metadata # - Loads or downloads movie credits # - Retrieves per-movie metadata # - Formats the final HTML output with photo, links, biography, and table # # The resulting HTML block is exported via the global `ACTOR_FILMOGRAPHY` # variable. This function is typically invoked from within blog update logic. # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor (input) # ACTOR_QUERY - Fallback actor name (input) # TMDB_API_KEY - API key for TMDb access # MY_MDB_CACHE - Base directory for local caches # DEBUG, VERBOSE - Logging flags # # Globals Set: # ACTOR_NAME, ACTOR_DOB, ACTOR_DOD, ACTOR_PHOTO_URL, # ACTOR_BLOG_URL, ACTOR_BIRTH_YEAR, ACTOR_TMDB_ID # ACTOR_FILMOGRAPHY - Final HTML output block # # Outputs: # - Echoes progress and diagnostics depending on VERBOSE and DEBUG flags # - Populates ACTOR_FILMOGRAPHY with full blog-compatible HTML # # Return: # 0 - Success # 1 - Any failure (API fetch error, cache corruption, missing keys) #============================================================================== GenerateActorFilmography() { local -r actor_name="${1}" local actor_id="" local birth_year="" local html="" local tmpfile tmpfile=$(mktemp) Echo "Looking up TMDb ID for '${actor_name}'..." actor_id=$(curl --silent --get "https://api.themoviedb.org/3/search/person" \ --data-urlencode "api_key=${TMDB_API_KEY}" \ --data-urlencode "query=${actor_name}" | jq -r '.results[0].id') if [[ -z "${actor_id}" || "${actor_id}" == "null" ]]; then EchoE "ERROR: Could not find TMDb ID for '${actor_name}'." return 1 fi birth_year=$(curl --silent --get "https://api.themoviedb.org/3/person/${actor_id}" \ --data-urlencode "api_key=${TMDB_API_KEY}" | jq -r '.birthday' | cut -d'-' -f1) Echo "Fetching movie credits for '${actor_name}'..." curl --silent --get "https://api.themoviedb.org/3/person/${actor_id}/movie_credits" \ --data-urlencode "api_key=${TMDB_API_KEY}" > "${tmpfile}" html+='<h2>Filmography: '"${actor_name}"'</h2>' html+='<table border="1" style="font-size: small;">' html+='<tr style="background-color:#004080;color:white;font-weight:bold;">' html+='<th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th></tr>' jq -r '.cast[] | [.release_date, .title, .character, .id] | @tsv' "${tmpfile}" | while IFS=$'\t' read -r release_date title role tmdb_movie_id; do [[ -z "${release_date}" || -z "${title}" ]] && continue local release_year="${release_date:0:4}" local imdb_id="" local age="?" # Age calculation if [[ -n "${birth_year}" && "${release_year}" =~ ^[0-9]{4}$ ]]; then age=$(( release_year - birth_year )) if (( age < 0 || age > 110 )); then age="?" fi fi imdb_id=$(curl --silent "https://api.themoviedb.org/3/movie/${tmdb_movie_id}/external_ids?api_key=${TMDB_API_KEY}" | jq -r '.imdb_id // empty') local safe_role=$(echo "${role}" | sed -E 's/&/&/g; s/</\</g; s/>/\>/g') local imdb_link="${title}" if [[ -n "${imdb_id}" && "${imdb_id}" != "null" ]]; then imdb_link="<a href=\"https://www.imdb.com/title/${imdb_id}\" target=\"_blank\">${title}</a>" fi (( VERBOSE > 1 )) && Echo " Actor: '${name}' Role: '${role}' Age: '${age}' Title: '${title}' (${release_date})" html+="<tr><td>${release_date}</td><td>${imdb_link}</td><td>${safe_role}</td><td>${age}</td><td>✓</td></tr>" done html+="</table>" rm -f "${tmpfile}" echo "${html}" } #============================================================================== # ResolveActorImdbId: Resolves actor name to IMDb ID via TMDb #------------------------------------------------------------------------------ # Description: # This function performs a two-step resolution: # # 1. Uses the actor name to retrieve the corresponding TMDb person ID # (via `GetActorTmdbId`, which queries TMDb). # 2. Uses that TMDb ID to fetch external identifiers and extract the # associated IMDb ID using the /external_ids endpoint. # # If any step fails, an error is printed and the function returns 1. # # Arguments: # ${1} - Actor name (e.g., "Jack Lemmon") # # Globals Used: # TMDB_API_KEY - The TMDb API key required for both queries # # Outputs: # - Echoes the resolved IMDb ID (e.g., nm0000493) to stdout # - Prints error messages via EchoE on failure # # Return: # 0 - IMDb ID was successfully resolved # 1 - Any error during TMDb resolution or API failure #============================================================================== ResolveActorImdbId() { local -r actor_name="${1}" local -r tmdb_id="$(GetActorTmdbId "${actor_name}")" if [[ -z "${tmdb_id}" || "${tmdb_id}" == "null" ]]; then EchoE "❌ Could not find TMDb ID for '${actor_name}'." return 1 fi local -r url="https://api.themoviedb.org/3/person/${tmdb_id}/external_ids?api_key=${TMDB_API_KEY}" local imdb_id imdb_id=$(curl --silent --fail "${url}" | jq -r '.imdb_id') if [[ -z "${imdb_id}" || "${imdb_id}" == "null" ]]; then EchoE "❌ Failed to resolve IMDb ID from TMDb ID ${tmdb_id}." return 1 fi echo "${imdb_id}" } #============================================================================== # GetTopActorsFromTmdb: Generate collapsible HTML block with top 5 actors #------------------------------------------------------------------------------ # Description: # Given an IMDb movie ID and its release year, this function: # # 1. Resolves the corresponding TMDb movie ID. # 2. Fetches the top cast members using the /credits endpoint. # 3. For each of the top 5 cast members: # - Fetches actor metadata (birth year) # - Calculates their age at the time of release # - Builds a row in an HTML <table> with actor name, role, and age # # The output is a collapsible <details> HTML section suitable for Blogger. # It includes clickable TMDb profile links for each actor. # # Arguments: # ${1} - IMDb ID of the movie (e.g., tt0030062) # ${2} - Year of movie release (used for age calculation) # # Globals Used: # TMDB_API_KEY - API key for TMDb # VERBOSE - If >1, prints actor details to the screen for debugging # DEBUG - Enables debug output for resolution failures # # Outputs: # Echoes an HTML string block (<details> + <table>) to stdout # # Return: # 0 - Success (even if partial or limited results) # 1 - TMDb ID resolution failure or missing actor data #============================================================================== GetTopActorsFromTmdb() { local -r imdb_id="${1}" local -r release_year="${2}" local tmdb_id tmdb_id=$(curl --silent "https://api.themoviedb.org/3/find/${imdb_id}?api_key=${TMDB_API_KEY}&external_source=imdb_id" | jq -r '.movie_results[0].id') if [[ -z "${tmdb_id}" || "${tmdb_id}" == "null" ]]; then (( DEBUG )) && EchoD "[${FUNCNAME[0]}] No TMDb ID found for IMDb: ${imdb_id}" return fi local cast_json cast_json=$(curl --silent "https://api.themoviedb.org/3/movie/${tmdb_id}/credits?api_key=${TMDB_API_KEY}") local html='<details style="margin-top:8px;"><summary style="cursor:pointer;font-weight:bold;">[+] Top Cast</summary>' html+='<table border="1" style="font-size: small; margin-top:5px;"><tr><th>Actor</th><th>Role</th><th>Age</th></tr>' for i in {0..5}; do local actor_id name role birthdate age actor_id=$(jq -r ".cast[${i}].id" <<<"${cast_json}") name=$(jq -r ".cast[${i}].name" <<<"${cast_json}") role=$(jq -r ".cast[${i}].character" <<<"${cast_json}") [[ -z "${actor_id}" || "${actor_id}" == "null" ]] && continue birthdate=$(curl --silent "https://api.themoviedb.org/3/person/${actor_id}?api_key=${TMDB_API_KEY}" | jq -r '.birthday') if [[ "${birthdate}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then local birth_year="${birthdate:0:4}" age=$(( year - birth_year )) else age="?" fi # Build HTML with hyperlink local actor_link="<a href=\"https://www.themoviedb.org/person/${actor_id}\" target=\"_blank\">${name}</a>" (( VERBOSE > 1 )) && Echo " Actor: '${name}' Role: '${role}' Age: '${age}'" html+="<tr><td>${actor_link}</td><td>${role}</td><td>${age}</td></tr>" done html+='</table></details>' echo "${html}" } #============================================================================== # GetTitleFromOmdb: Resolves proper English title and metadata using OMDb API #------------------------------------------------------------------------------ # Description: # Queries the OMDb API to fetch accurate metadata for a movie given its title # and expected release year. Results are cached in-memory using a global # associative array (METADATA_CACHE) to avoid redundant lookups. # # Fields retrieved include: # - Proper English title # - Genre(s) # - IMDb ID # - Runtime # - Poster URL # # If the API response year does not match the expected year, a warning is # issued and a fallback record is returned with the raw query string and "N/A" # placeholders. # # Arguments: # ${1} - Search query (movie title, as string) # ${2} - Expected release year (YYYY) # # Globals Used: # OMDB_API_KEY - OMDb API key # DEBUG - Enables debug output # METADATA_CACHE[] - Associative array for in-memory caching # # Outputs: # Echoes pipe-separated result string: # Title|||Genre|||IMDbID|||Runtime|||Poster # # Return: # 0 - Success (valid result or fallback returned) # 1 - None (all control handled internally; no external exit code) #============================================================================== GetTitleFromOmdb() { local -r query="${1}" local -r expected_year="${2}" local -r api_url="http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&t=$(urlencode "${query}")&y=${expected_year}&type=movie" # Check if already cached local cache_key="${query}_${expected_year}" if [[ -n "${METADATA_CACHE["${cache_key}"]}" ]]; then echo "${METADATA_CACHE["${cache_key}"]}" return fi local json json=$(curl --silent --max-time 10 "${api_url}") local success success=$(jq -r '.Response' <<< "${json}") local actual_year actual_year=$(jq -r '.Year // empty' <<< "${json}") # Check for success and valid year if [[ "${success}" == "True" && "${actual_year}" == "${expected_year}" ]]; then local proper_title=$(jq -r '.Title // empty' <<< "${json}") local genre=$(jq -r '.Genre // empty' <<< "${json}") local imdbid=$(jq -r '.imdbID // empty' <<< "${json}") local runtime=$(jq -r '.Runtime // empty' <<< "${json}") local poster=$(jq -r '.Poster // empty' <<< "${json}") local result="${proper_title}|||${genre}|||${imdbid}|||${runtime}|||${poster}" METADATA_CACHE["${cache_key}"]="${result}" (( DEBUG )) && EchoD "✅ OMDb title resolved: ${proper_title} [${genre}]" echo "${result}" else EchoW "⚠️ OMDb lookup mismatch or failure: Wanted ${expected_year}, got '${actual_year}' for '${query}'" local fallback="${query}|||N/A|||N/A|||N/A|||N/A" METADATA_CACHE["${cache_key}"]="${fallback}" echo "${fallback}" fi } #============================================================================== # GetActorTmdbId: Resolve TMDb person ID from actor name #------------------------------------------------------------------------------ # Description: # Queries the TMDb search endpoint with the given actor name to retrieve the # corresponding TMDb person ID. The query string is URL-encoded. This function # is typically used as the first step in resolving an actor's metadata from TMDb. # # Arguments: # ${1} - Actor name (e.g., "Jack Lemmon") # # Globals Used: # TMDB_API_KEY - Required TMDb API key # # Outputs: # Echoes the numeric TMDb ID to stdout if found # # Return: # 0 - Success (ID found and printed) # 1 - Failure (ID not found or TMDb error) #============================================================================== GetActorTmdbId() { local -r actor_name="${1}" local url="https://api.themoviedb.org/3/search/person?query=$(urlencode "${actor_name}")&api_key=${TMDB_API_KEY}" local tmdb_id tmdb_id=$(curl --silent --fail "${url}" | jq -r '.results[0].id') if [[ "${tmdb_id}" == "null" || -z "${tmdb_id}" ]]; then return 1 fi echo "${tmdb_id}" } #============================================================================== # GetActorPhotoUrl: Returns TMDb profile image URL for an actor #------------------------------------------------------------------------------ # Description: # Given an IMDb actor ID stored in ACTOR_IMDB_ID, this function queries TMDb's # "find" endpoint to retrieve the actor's profile image path. If a profile image # exists, it returns the full TMDb URL to a 300px-wide version of the image. # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor (e.g., nm0000493) # TMDB_API_KEY - TMDb API key for querying person metadata # # Outputs: # Echoes the full profile image URL (https://image.tmdb.org/...) or empty string # # Return: # 0 - Success (URL echoed or empty string) # 1 - Function will not exit with error; result is determined by output #============================================================================== GetActorPhotoUrl() { local -r tmdb_find_url="https://api.themoviedb.org/3/find/${ACTOR_IMDB_ID}?api_key=${TMDB_API_KEY}&external_source=imdb_id" local profile_path profile_path=$(curl --silent --fail "${tmdb_find_url}" | jq -r '.person_results[0].profile_path') if [[ -n "${profile_path}" && "${profile_path}" != "null" ]]; then echo "https://image.tmdb.org/t/p/w300${profile_path}" else echo "" fi } #============================================================================== # SubmitBlogDraft: Submits or updates a Blogger DRAFT post with HTML content #------------------------------------------------------------------------------ # Description: # Sends a draft post to the Blogger API using OAuth authentication. # If a path is provided, attempts to update an existing post; otherwise, # creates a new draft. In DRYRUN mode, outputs intended payload without posting. # # Globals: # BLOG_ACCESS_TOKEN - OAuth 2.0 Bearer token for Blogger API # BLOG_ID - Blogger blog ID to submit the post to # CUSTOM_TITLE - Title of the blog post (used if not overridden) # DRYRUN - If set to 1, skips API call and shows simulated output # DEBUG - If set, emits debug logs using EchoD # # Arguments: # ${1} - HTML body content (unescaped raw HTML) # ${2} - Optional post path (e.g., "2025/04/some-title.html") to update # # Outputs: # On success, echoes the full Blogger post URL (https://...) # On failure, prints error message and returns non-zero exit status #============================================================================== SubmitBlogDraft() { local -r html_content="${1}" local -r post_path="${2:-}" local -r blog_title="${CUSTOM_TITLE:-Actor - ${ACTOR_QUERY} Filmography}" (( DEBUG )) && EchoD "${FUNCNAME[0]}| Title=${blog_title}" (( DEBUG )) && EchoD "${FUNCNAME[0]}| Update=${post_path:-<new>}" if (( DRYRUN )); then Echo "[DRY-RUN] Blog post would be submitted with title: ${blog_title}" Echo "[DRY-RUN] HTML Content:" Echo "${html_content}" return 0 fi local -r payload="$( jq -n \ --arg title "${blog_title}" \ --arg content "${html_content}" \ --arg blog_id "${BLOG_ID}" \ '{ kind: "blogger#post", blog: { id: $blog_id }, title: $title, content: $content }' )" local response local url if [[ -n "${post_path}" ]]; then response="$(curl --silent --fail --show-error --request PUT \ --header "Authorization: Bearer ${BLOG_ACCESS_TOKEN}" \ --header "Content-Type: application/json" \ --data "${payload}" \ "https://www.googleapis.com/blogger/v3/blogs/${BLOG_ID}/posts/bypath?path=${post_path}")" url="$(jq -r '.url // empty' <<< "${response}")" if [[ -z "${url}" ]]; then EchoE "❌ Blogger update failed. Response: ${response}" return 1 fi echo "${url}" else response="$(curl --silent --fail --show-error --request POST \ --header "Authorization: Bearer ${BLOG_ACCESS_TOKEN}" \ --header "Content-Type: application/json" \ --data "${payload}" \ "https://www.googleapis.com/blogger/v3/blogs/${BLOG_ID}/posts/")" url="$(jq -r '.url // empty' <<< "${response}")" if [[ -z "${url}" ]]; then EchoE "❌ Blogger submission failed. Response: ${response}" return 1 fi echo "${url}" fi } #============================================================================== # BuildHtmlTable: Constructs the grouped HTML table from collected movie data #------------------------------------------------------------------------------ # Description: # Uses the associative array `year_map` (year → HTML row list) to build a # full HTML <table> with one row per year and a bullet list of updated or # added movie titles. Sorting is done alphabetically within each year using # ASCII-safe, case-insensitive sorting (LC_ALL=C). # # Globals Used: # REFERENCE_DATE - Date string used in header intro # year_map - Associative array: [year] → list of raw HTML lines # # Outputs: # Echoes full HTML block to stdout # # Return: # 0 - Success #============================================================================== BuildHtmlTable() { local -r intro="These are the movie titles added (new titles) or updated (new format for an existing title) on ${REFERENCE_DATE}:<br><br>" local html='<!-- Generated by MovieUpdateDraft.sh -->' html+='<table border="1">' html+='<tr style="background-color:#004080;color:white;font-weight:bold;"><th>Year</th><th>Updated Titles</th></tr>' for year in $(printf "%s\n" "${!year_map[@]}" | sort); do local raw_entries IFS=$'\n' read -rd '' -a raw_entries < <(printf "%s\n" "${year_map[$year]}" | grep -v '^\s*$' && printf '\0') # Build sortable pairs: TITLE|||HTML_LINE local -a sortable_lines=() for line in "${raw_entries[@]}"; do line=$(echo "${line}" | tr -d '\r\n') # Try to extract title from anchor tag local display_text if [[ "${line}" =~ $'^<a [^>]*>([^<]+)</a>' ]]; then display_text="${BASH_REMATCH[1]}" else # Fallback: use first few words from line display_text=$(echo "${line}" | sed -E 's/^([^<]+) .*/\1/') fi # Remove any multibyte/UTF-8 characters just for sorting display_text=$(echo "${display_text}" | iconv -c -f UTF-8 -t ASCII//TRANSLIT) sortable_lines+=( "${display_text}|||${line}" ) done # Sort by display title (case-insensitive, ASCII-safe) local -a sorted_lines=() IFS=$'\n' read -rd '' -a sorted_lines < <( printf "%s\n" "${sortable_lines[@]}" | LC_ALL=C sort -f | sed 's/^[^|]*|||//' && printf '\0' ) html+="<tr><td>${year}</td><td><ul>" for sorted_line in "${sorted_lines[@]}"; do html+="<li>${sorted_line}</li>" done html+="</ul></td></tr>" done html+="</table>" printf "%s" "${intro}${html}" } #============================================================================== # BuildSummaryHtml: Generate a visual summary block for updated movie entries #------------------------------------------------------------------------------ # Description: # Constructs an HTML-formatted summary of changes made for the reference # date. It includes total counts (years, titles, added/updated) and a # breakdown by year. Uses emojis and bold formatting for clarity. # # Globals Used: # year_map - Associative array of year → titles # year_added_count - Associative array of year → count of added titles # year_changed_count - Associative array of year → count of updated titles # total_added - Total number of newly added titles # total_changed - Total number of updated titles # # Outputs: # Echoes a full HTML <ul>-formatted block with summary data to stdout # # Return: # 0 - Success #============================================================================== BuildSummaryHtml() { local -i total_years=${#year_map[@]} local -i total_titles=$((total_added + total_changed)) local html='<br><br><b>Summary:</b><br>' html+='<ul>' html+="<li>🗓️ Total Years Updated: <b>${total_years}</b></li>" html+="<li>🎬 Total Titles: <b>${total_titles}</b></li>" html+="<li>🆕 New Titles: <b>${total_added}</b></li>" html+="<li>♻️ Updated Titles: <b>${total_changed}</b></li>" html+='</ul>' html+='<b>Details per Year:</b><br>' html+='<ul>' for year in $(printf "%s\n" "${!year_map[@]}" | sort); do local -i added=${year_added_count["${year}"]:-0} local -i changed=${year_changed_count["${year}"]:-0} html+="<li><b>${year}</b>: 🆕 ${added}, ♻️ ${changed}</li>" done html+='</ul>' printf "%s" "${html}" } #============================================================================== # ScanDirectories: Traverse movie directories and detect updates by date #------------------------------------------------------------------------------ # Description: # Walks through all year-based subdirectories under ${MOVIE_ROOT}, identifying # directories that have been modified or added based on file timestamps # between REFERENCE_DATE_EPOCH and TODAY_EPOCH. For each qualifying folder: # - Determines if it is a new title or an update # - Retrieves OMDb metadata (title, genre, runtime, IMDb ID) # - Optionally fetches top actors from TMDb (if enabled) # - Formats HTML blocks with poster, cast, and metadata # # Updates year_map[], year_added_count[], year_changed_count[], # total_added and total_changed globals accordingly. # # Globals Used: # MOVIE_ROOT - Root path to year-based movie folders # REFERENCE_DATE - Target date in YYYY-MM-DD format # REFERENCE_DATE_EPOCH - Epoch of reference date # TODAY_EPOCH - Epoch of today’s date # VERBOSE - Controls verbosity of output # DEBUG - Enables debug logging # FILTER_YEARS[] - Optional filter list of years to include # FORCE_ACTORS - If set, actor info is always fetched # TMDB_KEY_VALID - TMDb key validation result # year_map[] - Associative array for HTML generation # year_added_count[] - Count of added titles per year # year_changed_count[] - Count of updated titles per year # total_added - Total count of new titles # total_changed - Total count of changed titles # # Outputs: # Populates year_map with resolved HTML snippets per year # Updates count globals for summary reporting # # Return: # 0 - Always #============================================================================== ScanDirectories() { Echo "Scanning directories for updates on ${REFERENCE_DATE}..." # Stats tracking declare -gA year_added_count=() declare -gA year_changed_count=() declare -gi total_added=0 declare -gi total_changed=0 for year_dir in "${MOVIE_ROOT}"/*; do [[ -d "${year_dir}" ]] || continue [[ "${year_dir##*/}" =~ ^! ]] && continue local year="${year_dir##*/}" [[ "${year}" =~ ^[0-9]{4}$ ]] || continue # Apply year filter if provided if [[ ${#FILTER_YEARS[@]} -gt 0 ]]; then local -i match=0 for filter in "${FILTER_YEARS[@]}"; do [[ "${year}" == "${filter}" ]] && match=1 && break done (( match == 0 )) && continue fi Echo "Processing year: ${year}" while IFS= read -r -d '' title_dir; do [[ "${title_dir##*/}" =~ ^! ]] && continue local folder_name="${title_dir##*/}" (( DEBUG )) && [[ -z "${folder_name}" ]] && EchoD "⚠️ Empty folder name extracted from: ${title_dir}" [[ -z "${folder_name}" ]] && continue (( DEBUG )) && EchoD " Checking title: ${folder_name}" local -i matched=0 local -i is_added=1 while IFS= read -r -d '' file; do local target target=$(readlink -f "${file}") [[ -z "${target}" ]] && continue local -i file_epoch file_epoch=$(stat -c %Y "${target}") (( DEBUG )) && EchoD " File: ${file} -> Target: ${target} (Epoch: ${file_epoch})" if (( file_epoch >= REFERENCE_DATE_EPOCH && file_epoch < TODAY_EPOCH )); then [[ -e "${title_dir}/metadata.json" ]] && is_added=0 local update_type update_type=$([[ ${is_added} -eq 1 ]] && echo "Added" || echo "Changed") # Count BEFORE OMDb (even if OMDb fails) if (( is_added )); then (( year_added_count["${year}"]++ )) (( total_added++ )) else (( year_changed_count["${year}"]++ )) (( total_changed++ )) fi # OMDb call local resolved_full resolved_full=$(GetTitleFromOmdb "${folder_name}" "${year}") local resolved_title="${resolved_full%%|||*}" resolved_title="$(echo "${resolved_title}" | xargs)" # <-- Trim after assigning EchoD "resolved_title='${resolved_title}' from resolved_full='${resolved_full}'" if [[ -z "${resolved_title}" || "${resolved_title}" == "null" ]]; then resolved_title="${folder_name}" (( DEBUG )) && EchoD "Fallback: using folder name as title: '${resolved_title}'" fi # Verbose output of matched title if (( VERBOSE == 1 )); then Echo " ${resolved_title} (updated ${REFERENCE_DATE})" elif (( VERBOSE > 1 )); then Echo " -> MATCHED: ${resolved_title} (updated ${REFERENCE_DATE})" fi local temp_rest="${resolved_full#*|||}" local resolved_genre="${temp_rest%%|||*}" temp_rest="${temp_rest#*|||}" local resolved_imdbid="${temp_rest%%|||*}" temp_rest="${temp_rest#*|||}" local resolved_runtime="${temp_rest%%|||*}" local resolved_poster="${temp_rest#*|||}" (( DEBUG )) && EchoD "Calling TMDb with IMDb ID: ${resolved_imdbid}" if (( FORCE_ACTORS )) || (( TMDB_KEY_VALID )); then cast_html="$(GetTopActorsFromTmdb "${resolved_imdbid}" "${year}")" #EchoD "='${cast_html}'" fi local poster_html="" # Start table if either poster or cast exists if [[ -n "${resolved_poster}" && "${resolved_poster}" != "N/A" || -n "${cast_html}" ]]; then poster_html='<table><tr>' # Poster column if [[ -n "${resolved_poster}" && "${resolved_poster}" != "N/A" ]]; then poster_html+='<td style="vertical-align:top;">' poster_html+='<img src="'"${resolved_poster}"'" width="120">' poster_html+='</td>' fi # Cast column if [[ -n "${cast_html}" ]]; then poster_html+='<td style="padding-left:8px; vertical-align:top;">' poster_html+="${cast_html}" poster_html+='</td>' fi poster_html+='</tr></table>' fi # Formatting local genre_suffix="" if [[ -n "${resolved_genre}" && "${resolved_genre}" != "N/A" ]]; then genre_suffix=" <span style=\"color:#800080;font-weight:bold;\">[${resolved_genre}, ${resolved_runtime}]</span>" fi local linked_title="${resolved_title}" if [[ -n "${resolved_imdbid}" && "${resolved_imdbid}" != "N/A" ]]; then linked_title="<a href=\"https://www.imdb.com/title/${resolved_imdbid}\" target=\"_blank\">${resolved_title}</a>" fi (( DEBUG )) && EchoD "✔ Counted '${resolved_title}' as ${update_type} for year ${year}" year_map["${year}"]+="${linked_title}${genre_suffix} (${update_type})${poster_html}"$'\n' matched=1 break fi done < <(find "${title_dir}" -type l -print0) (( matched == 0 && VERBOSE > 1 )) && Echo " -> No file matched." done < <(find "${year_dir}" -mindepth 1 -maxdepth 1 -type d -print0) done } #============================================================================== # ParseArguments: Parse all command-line arguments and populate global variables. #------------------------------------------------------------------------------ # Description: # Parses CLI flags to determine execution mode (e.g., --actor or --movie-list), # along with filters, toggles, and post behavior. Sets global control variables. # # Globals Set: # EXECUTION_MODE, EXECUTION_SUBMODE, ACTOR_QUERY, IMDB_ID, # DRYRUN, VERBOSE, DEBUG, FORCE, FORCE_ACTORS, FORCE_PUBLISH, # CUSTOM_DATE, CUSTOM_TITLE, LABELS[], FILTER_YEARS[], # CHECK_DEPS_DEFAULT, CHECK_DEPS_LOCAL, CHECK_DEPS_INSTALL, # VERBOSE_DEPS, USE_HTML, UPDATE_MODE, NEW_BLOGGER_URL, REVERT_STEPS # # Outputs: # - Populates globals based on flags # - Echoes errors and exits on invalid combinations #============================================================================== ParseArguments() { local -i nActorSwitchUsed=0 local -i nMovieListUsed=0 NEW_BLOGGER_URL= while [[ $# -gt 0 ]]; do case ${1} in -h|--help) PrintHelp; exit 0 ;; -V|--version) Echo "Version: ${SCRIPT_VERSION}"; exit 0 ;; -n|--dry-run) DRYRUN=1 ; shift ;; -v|--verbose) ((VERBOSE++)) ; shift ;; -D|--debug) DEBUG=1 ; shift ;; -d|--date) CUSTOM_DATE="${2}" ; shift 2 ;; --check-deps) CHECK_DEPS_DEFAULT=1 ; shift ;; --check-deps-local) CHECK_DEPS_LOCAL=1 ; shift ;; --check-deps-install) CHECK_DEPS_INSTALL=1 ; shift ;; #-------------------------------------- ACTOR MODE --actor) shift if [[ -z "$1" ]]; then EchoE "--actor requires a name (e.g., --actor 'Jack Lemmon')" exit 1 fi ACTOR_QUERY="$1" EXECUTION_MODE=2 EXECUTION_SUBMODE=0 nActorSwitchUsed=1 shift ;; --force) FORCE=1 ; shift ;; --imdb) (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; } shift if [[ -z "$1" || ! "$1" =~ ^nm[0-9]{7,}$ ]]; then EchoE "--imdb requires a valid IMDb ID (e.g., nm0000493)" exit 1 fi IMDB_ID="$1" shift ;; --show-blogger-url) (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; } EXECUTION_SUBMODE=1 shift ;; --delete-blogger-url) (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; } EXECUTION_SUBMODE=2 shift ;; --update-blogger-url) (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; } shift if [[ -z "$1" ]]; then EchoE "--update-blogger-url requires a URL value" exit 1 fi EXECUTION_SUBMODE=3 NEW_BLOGGER_URL="$1" (( DEBUG )) && EchoD "NEW_BLOGGER_URL='${NEW_BLOGGER_URL}'" shift ;; --revert-blogger-url) (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; } REVERT_STEPS="${2:-1}" EXECUTION_SUBMODE=4 shift 2 ;; --history-blogger-url) (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; } EXECUTION_SUBMODE=5 shift ;; --force-bio) (( nActorSwitchUsed==0 )) && { EchoE "$1 can only be used with --actor" ; exit 1; } FORCE_BIO=1 shift ;; #-------------------------------------- MOVIE LIST MODE --movie-list) if (( EXECUTION_MODE != 0 )); then EchoE "--movie-list cannot be used with another execution mode." exit 1 fi EXECUTION_MODE=1 nMovieListUsed=1 shift ;; --update) shift UPDATE_MODE=1 if [[ -n "${1}" && "${1}" != --* ]]; then ACTOR_UPDATE_PATH="${1}" shift fi ;; --force-actors) FORCE_ACTORS=1 ; shift ;; --no-html) USE_HTML=0 ; shift ;; --publish) FORCE_PUBLISH=1 ; shift ;; -t|--title) shift; CUSTOM_TITLE="$1"; shift ;; -l|--label) shift; LABELS+=("$1"); shift ;; -y|--year) [[ "$2" =~ ^[0-9]{4}$ ]] || { EchoE "Invalid year format: $2" ; exit 1; } FILTER_YEARS+=("$2") shift 2 ;; -r|--range) shift if [[ "$1" =~ ^([0-9]{4})-([0-9]{4})$ ]]; then start=${BASH_REMATCH[1]} end=${BASH_REMATCH[2]} elif [[ "$1" =~ ^([0-9]{4})-$ ]]; then start=${BASH_REMATCH[1]} end=$(( start + 9 )) elif [[ "$1" =~ ^-([0-9]{4})$ ]]; then end=${BASH_REMATCH[1]} start=$(( end - (end % 10) )) else EchoE "Invalid range format: $1" exit 1 fi if (( start > end )); then EchoE "Invalid range: ${start}-${end}" exit 1 fi for (( y=start; y<=end; y++ )); do FILTER_YEARS+=("${y}") done shift ;; --) shift ; break ;; -*) EchoE "Unknown option: ${1}" ; PrintHelp ; exit 1 ;; *) POSITIONAL_ARGS+=("${1}") ; shift ;; esac done (( nMovieListUsed==1 && nActorSwitchUsed==1 )) && { EchoE "--movie-list cannot be combined with --actor"; exit 1; } (( nMovieListUsed==0 && nActorSwitchUsed==0 )) && { EchoE "No valid mode specified. Use --movie-list or --actor"; exit 1; } if (( nActorSwitchUsed==1 )); then if [[ -n "${ACTOR_QUERY}" && -n "${IMDB_ID}" ]]; then EchoW "Both --actor and --imdb specified. IMDb ID will take precedence." ACTOR_QUERY="" fi fi } #============================================================================== # DoDependency: Validate or install required tools with fallback and verbosity #------------------------------------------------------------------------------ # Description: # Checks for required and optional dependencies: # - Required: curl, jq, date, urlencode # - Optional (feature-specific): pup # # Mode of operation is controlled by: # - CHECK_DEPENDENCIES: verify and report missing (no fallback/install) # - CHECK_DEPS_LOCAL: check and fallback if possible (e.g., DefineUrlencodeFallback) # - CHECK_DEPS_INSTALL: force install mode (assumes package manager is available) # - VERBOSE_DEPS: if set, displays version info for available tools # # Globals Used: # CHECK_DEPENDENCIES # CHECK_DEPS_LOCAL # CHECK_DEPS_INSTALL # VERBOSE_DEPS # # Outputs: # - Echo output for each tool, including version or status # - Fallback logic invoked for 'urlencode' if missing and local mode enabled # - Exits with code 1 if any required tool is missing and not handled #============================================================================== DoDependency() { local -a required_tools=("curl" "jq" "date" "urlencode") local -a optional_tools=("pup") local -i missing=0 if (( CHECK_DEPS_INSTALL )); then Echo "🔧 Forcing install of all required tools..." for tool in "${required_tools[@]}"; do EnsureDependency "${tool}" "${tool}" done for opt in "${optional_tools[@]}"; do EnsureDependency "${opt}" "${opt}" "optional" done Echo "✅ All required tools have been installed or handled." elif (( CHECK_DEPS_LOCAL )); then Echo "🔍 Checking tools and applying local fallback logic (no install)..." for tool in "${required_tools[@]}"; do if command -v "${tool}" &>/dev/null; then Echo "✔ '${tool}' available" (( VERBOSE_DEPS )) && "${tool}" --version 2>&1 | head -n 1 | sed 's/^/ ↪ /' else case "${tool}" in urlencode) DefineUrlencodeFallback Echo "ℹ️ Defined fallback for '${tool}'" ;; *) EchoE "❌ Required tool '${tool}' is missing and no fallback is available." (( missing++ )) ;; esac fi done for opt in "${optional_tools[@]}"; do if command -v "${opt}" &>/dev/null; then Echo "✔ Optional tool '${opt}' available" (( VERBOSE_DEPS )) && "${opt}" --version 2>&1 | head -n 1 | sed 's/^/ ↪ /' else EchoW "⚠️ Optional tool '${opt}' is not available. Some features may be disabled." fi done if (( missing > 0 )); then EchoE "⛔ ${missing} required tool(s) could not be handled. Aborting." exit 1 else Echo "✅ All required tools are available or have been handled with fallback." fi elif (( CHECK_DEPENDENCIES )); then Echo "🔍 Verifying dependencies only (no install, no fallback)..." for tool in "${required_tools[@]}"; do if command -v "${tool}" &>/dev/null; then Echo "✔ '${tool}' available" (( VERBOSE_DEPS )) && "${tool}" --version 2>&1 | head -n 1 | sed 's/^/ ↪ /' else EchoE "❌ Missing required tool: ${tool}" (( missing++ )) fi done for opt in "${optional_tools[@]}"; do if command -v "${opt}" &>/dev/null; then Echo "✔ Optional tool '${opt}' available" (( VERBOSE_DEPS )) && "${opt}" --version 2>&1 | head -n 1 | sed 's/^/ ↪ /' else EchoW "⚠️ Optional tool '${opt}' is not available. Some features may be disabled." fi done if (( missing > 0 )); then EchoE "⛔ Missing ${missing} required tool(s). Use --check-deps-local or --check-deps-install." exit 1 else Echo "✅ All required tools are present." fi fi } #============================================================================== # ParseImdbMiniBio: Extracts the Mini Bio HTML section from downloaded IMDb bio #------------------------------------------------------------------------------ # Description: # Parses a downloaded IMDb biography HTML file and extracts the "Mini Bio" # section. It targets the block identified by 'data-testid="sub-section-mini_bio"' # and cleans the HTML into plain text. The result is saved to the output file. # # Arguments: # ${1} - Path to the input HTML file downloaded from IMDb # ${2} - Path to the output text file where the parsed bio should be saved # # Globals: # FUNCNAME - Used for logging/debug output # # Outputs: # Writes plain-text biography content to the specified output file. # # Exit Codes: # None explicitly. Errors may surface from sed or file access issues. #============================================================================== ParseImdbMiniBio() { local -r input_file="${1}" local -r output_file="${2}" # Attempt to extract bio inside 'data-testid="sub-section-mini_bio"' block sed -n '/data-testid="sub-section-mini_bio"/,/<\/section>/p' "${input_file}" | sed -n '/<div class="ipc-html-content-inner-div"/,/<\/div><\/div><\/div>/p' | sed 's/<[^>]*>//g; s/"/"/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 mini_bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db" ACTOR_BIO_HTML= if (( ! FORCE_BIO )) && [[ -s "${mini_bio_file}" ]]; then ACTOR_BIO_HTML="$(< "${mini_bio_file}")" return 0 fi local imdb_bio_url="https://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm" local tmp_bio_html tmp_bio_html="$(mktemp)" # Use spoofed User-Agent to bypass 403 #curl --silent --fail \ # --header "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" \ # "${imdb_bio_url}" > "${tmp_bio_html}" wget --quiet \ --header="User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" \ --output-document="${tmp_bio_html}" \ "${imdb_bio_url}" ParseImdbMiniBio "${tmp_bio_html}" "${mini_bio_file}" rm -f "${tmp_bio_html}" # Validate and set memory cache if [[ ! -s "${mini_bio_file}" ]]; then EchoW "${FUNCNAME[0]}:${LINENO}: IMDb Mini Bio could not be extracted: ${mini_bio_file}" ACTOR_BIO_HTML="<p><em>Biography not available.</em></p>" echo "${ACTOR_BIO_HTML}" > "${mini_bio_file}" else ACTOR_BIO_HTML="$(< "${mini_bio_file}")" EchoD "${FUNCNAME[0]}:${LINENO}:✅ IMDb Mini Bio saved to ${mini_bio_file}" fi local links_file="${MY_MDB_CACHE}/links.db" mkdir -p "$(dirname "${links_file}")" if ! grep -q "^${imdb_id}[[:space:]]+.*#mini_bio" "${links_file}" 2>/dev/null; then echo -e "${imdb_id}\thttps://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm#mini_bio" >> "${links_file}" EchoD "${FUNCNAME[0]}:${LINENO}:🔗 Added IMDb Mini Bio link to ${links_file}" fi } #============================================================================== # GenerateActorFilmographyFromTmdb: Build full actor filmography HTML from TMDb #------------------------------------------------------------------------------ # Description: # Retrieves filmography for an actor using TMDb API, falling back to local # cache when available. The output includes photo, bio, movie table, and # external links. Movies are fetched via movie_credits API and sorted by # release date. Metadata is stored in ~/.MyBlogCache/*.db files. # # If DRYRUN is enabled, metadata will not be saved. Roles are sanitized for # HTML, and a red ❌ is shown for films released after the actor’s death. # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor (required) # ACTOR_QUERY - Actor name for fallback messaging # ACTOR_NAME - Actor name (exported here) # ACTOR_DOB - Date of birth, YYYY-MM-DD (exported here) # ACTOR_DOD - Date of death, if any (exported here) # ACTOR_BIRTH_YEAR - Year of birth as integer (exported here) # ACTOR_PHOTO_URL - Full TMDb profile image URL (exported here) # ACTOR_BLOG_URL - Cached Blogger post URL (exported here) # ACTOR_TMDB_ID - TMDb actor ID (set internally) # ACTOR_FILMOGRAPHY - HTML output block (set by this function) # TMDB_API_KEY - TMDb API key # MY_MDB_CACHE - Path to ~/.MyBlogCache # DRYRUN - If set, avoid writing to any cache # VERBOSE - Controls debug/info output # # Outputs: # Populates ACTOR_FILMOGRAPHY with HTML content including: # - Actor photo and details # - Chronological movie table (Title, Year, Role, Age, IMDb link, TMDb ID) # - External links from links.db # - Biography from <imdb>.bio.db # # Exit Codes: # 0 - Success # 1 - Failure to fetch TMDb ID, movie credits, or generate output #============================================================================== GenerateActorFilmographyFromTmdb() { local -r imdb_id="${ACTOR_IMDB_ID}" local record tmdb_id name dob dod photo_url blog_url local movie role release title year age age_cell imdb_link tmdb_link watched_date blogger_url photo_url DBMovie_InitActorDb DBMovie_InitMovieCreditsDb DBMovie_InitMovieDb record=$(DBMovie_LoadActorRecord "${imdb_id}") if [[ -n "${record}" ]]; then IFS=$'\t' read -r _ tmdb_id name dob dod photo_url blog_url <<< "${record}" export ACTOR_NAME="${name}" export ACTOR_DOB="${dob}" export ACTOR_DOD="${dod}" export ACTOR_PHOTO_URL="${photo_url}" export ACTOR_BLOG_URL="${blog_url}" export ACTOR_BIRTH_YEAR="${dob:0:4}" ACTOR_TMDB_ID="${tmdb_id}" Echo "📇 Loaded actor from cache: ${ACTOR_NAME} (${dob}–${dod})" else EchoW "🔍 No match found in local cache. Fetching data for '${ACTOR_NAME}' from TMDb..." tmdb_id=$(curl --silent --fail "https://api.themoviedb.org/3/find/${imdb_id}?api_key=${TMDB_API_KEY}&external_source=imdb_id" | jq -r '.person_results[0].id') if [[ -z "${tmdb_id}" || "${tmdb_id}" == "null" ]]; then EchoE "Could not find TMDb ID for '${imdb_id}'." return 1 fi details=$(curl --silent --fail "https://api.themoviedb.org/3/person/${tmdb_id}?api_key=${TMDB_API_KEY}") name=$(jq -r '.name' <<< "${details}") dob=$(jq -r '.birthday // empty' <<< "${details}") dod=$(jq -r '.deathday // empty' <<< "${details}") photo_url=$(jq -r '.profile_path // empty' <<< "${details}") [[ -n "${photo_url}" ]] && photo_url="https://image.tmdb.org/t/p/w500${photo_url}" DBMovie_SaveActorRecord "${imdb_id}" "${tmdb_id}" "${name}" "${dob}" "${dod}" "${photo_url}" "" # IMDb Mini Bio: download and extract the main paragraph local imdb_bio_url="https://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm" local mini_bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db" local tmp_bio_html tmp_bio_html="$(mktemp)" curl --silent --fail "${imdb_bio_url}" > "${tmp_bio_html}" # Try using pup (HTML parser), fallback to grep/sed if command -v pup >/dev/null 2>&1; then pup 'h4:contains("Mini Bio") + .soda text{}' < "${tmp_bio_html}" | \ sed '/^[[:space:]]*$/d' > "${mini_bio_file}" else sed -n '/Mini Bio/,/<h4 class="li_group">/p' "${tmp_bio_html}" | sed -n '/<div class="soda[^>]*">/,/<\/div>/p' > "${mini_bio_file}" fi # Validate output if [[ ! -s "${mini_bio_file}" ]]; then EchoW "${FUNCNAME[0]}:${LINENO}: IMDb Mini Bio could not be extracted: ${mini_bio_file}" echo "<p><em>Biography not available.</em></p>" > "${mini_bio_file}" else EchoD "${FUNCNAME[0]}:${LINENO}:✅ IMDb Mini Bio saved to ${mini_bio_file}" fi rm -f "${tmp_bio_html}" # Append Mini Bio permalink to links.db local links_file="${MY_MDB_CACHE}/links.db" mkdir -p "$(dirname "${links_file}")" if ! grep -q "^${imdb_id}[[:space:]]+.*#mini_bio" "${links_file}" 2>/dev/null; then echo -e "${imdb_id}\thttps://www.imdb.com/name/${imdb_id}/bio/?ref_=nm_ov_bio_sm#mini_bio" >> "${links_file}" EchoD "${FUNCNAME[0]}:${LINENO}:🔗 Added IMDb Mini Bio link to ${links_file}" fi EchoD "🆕 Added '${name}' to cache (IMDb: ${imdb_id})" export ACTOR_NAME="${name}" export ACTOR_DOB="${dob}" export ACTOR_DOD="${dod}" export ACTOR_PHOTO_URL="${photo_url}" export ACTOR_BLOG_URL="" export ACTOR_BIRTH_YEAR="${dob:0:4}" ACTOR_TMDB_ID="${tmdb_id}" fi EnsureActorMiniBioCached "${imdb_id}" credits_cached=$(DBMovie_LoadMovieCreditsByActor "${ACTOR_TMDB_ID}") || credits_cached="" if [[ -z "${credits_cached}" ]]; then EchoW "📥 No credits found in local cache for TMDb actor ID ${ACTOR_TMDB_ID}, fetching..." credits_json=$(curl --silent --fail \ "https://api.themoviedb.org/3/person/${ACTOR_TMDB_ID}/movie_credits?api_key=${TMDB_API_KEY}") if [[ -z "${credits_json}" ]]; then EchoE "Failed to fetch credits from TMDb." return 1 fi downloaded_count=0 echo "${credits_json}" | jq -r '[.cast[] | select(.release_date != null and .title != null and .character != null)] | sort_by(.release_date)[] | [.id, .title, .character] | @tsv' | while IFS=$'\t' read -r movie_id title role; do DBMovie_SaveMovieCredit "${movie_id}" "${ACTOR_TMDB_ID}" "${role}" (( downloaded_count++ )) done (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}:📦 Downloaded and cached ${downloaded_count} filmography entries for ${ACTOR_NAME}." credits_cached=$(DBMovie_LoadMovieCreditsByActor "${ACTOR_TMDB_ID}") || { EchoE "Failed to read back credits after saving." return 1 } else cached_lines=$(echo "${credits_cached}" | wc -l) Echo "✅ Using cached movie credits for TMDb ID ${ACTOR_TMDB_ID} (${cached_lines} records)" fi output="" (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: ACTOR_PHOTO_URL='${ACTOR_PHOTO_URL}'" if [[ -n "${ACTOR_PHOTO_URL}" ]]; then output+="<div style=\"display:flex; align-items:flex-start; margin-bottom:20px;\">" output+="<img src=\"${ACTOR_PHOTO_URL}\" alt=\"${ACTOR_NAME}\" style=\"width:160px; margin-right:20px; border:1px solid #ccc;\" />" output+="<div style=\"font-size:16px; line-height:1.6;\">" output+="<strong>${ACTOR_NAME}</strong><br />" output+="Born: ${ACTOR_DOB}<br />" output+="Died: ${ACTOR_DOD}" output+="</div></div>" fi output+="<h2>Filmography</h2>" output+="<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">" output+="<tr><th>Release</th><th>Title</th><th>Role</th><th>Age</th><th>Watch</th><th>IMDb</th><th>TMDb</th></tr>" local movie_tmdb_id movie_imdb_id country title original_title release watched_date movie_blogger_url movie_photo_url local role year age age_cell imdb_link tmdb_link while IFS='|' read -r movie_tmdb_id movie_imdb_id country title original_title release watched_date movie_blogger_url movie_photo_url; do (( DEBUG )) && EchoD "movie_tmdb_id='${movie_tmdb_id}'" role=$(DBMovie_GetMovieRole "${movie_tmdb_id}" "${ACTOR_TMDB_ID}") year="${release:0:4}" (( VERBOSE )) && Echo "🎬 Processing: ${title} (${year})" # Start of a row output+="<tr>" # Movie release year output+="<td>${year}</td>" # Movie title, with link to my review in blogger if [[ -n "${movie_blogger_url}" ]]; then output+="<td><a href=\"https://afberendsen.blogspot.com/${movie_blogger_url}\" target=\"_blank\">${title}</a></td>" else output+="<td>${title}</td>" fi output+="<td>${role}</td>" # Actor age at movie age="" age_cell="" if [[ -n "${ACTOR_DOB}" && -n "${release}" ]]; then age=$(( ${release:0:4} - ${ACTOR_DOB:0:4} )) if [[ -n "${ACTOR_DOD}" && "${release}" > "${ACTOR_DOD}" ]]; then age_cell="❌" else age_cell="${age}" fi fi output+="<td>${age_cell}</td>" # Watched if [[ -n "${watched_date}" ]]; then output+="<td>✅</td>" else output+="<td></td>" fi # IMDB link, if any if [[ -n "${movie_imdb_id}" ]]; then output+="<td><a href=\"https://www.imdb.com/title/${movie_imdb_id}\" target=\"_blank\">${movie_imdb_id}</a></td>" else output+="<td><span style=\"color:#999;\">N/A</span></td>" fi # TMDB link, if any if [[ -n "${movie_tmdb_id}" ]]; then output+="<td><a href=\"https://www.themoviedb.org/movie/${movie_tmdb_id}\" target=\"_blank\">${movie_tmdb_id}</a></td>" else output+="<td><span style=\"color:#999;\">N/A</span></td>" fi # End of row output+="<tr>" done < <(DBMovie_LoadMoviesByActor "${ACTOR_TMDB_ID}" | sort -t'|' -k6) # end of filmography table output+="</tbody></table>" # begining of Links header output+="<h2>Links</h2><ul>" # 1. IMDb actor page output+="<li><a href=\"https://www.imdb.com/name/${imdb_id}\" target=\"_blank\">IMDb: ${ACTOR_NAME}</a></li>" # 2. TMDb actor page if [[ -n "${ACTOR_TMDB_ID}" ]]; then output+="<li><a href=\"https://www.themoviedb.org/person/${ACTOR_TMDB_ID}\" target=\"_blank\">TMDb: ${ACTOR_NAME}</a></li>" fi # 3. Additional custom links from links.db local links_file="${MY_MDB_CACHE}/links.db" if [[ -f "${links_file}" ]]; then while read -r dummy url; do output+="<li><a href=\"${url}\" target=\"_blank\">${url}</a></li>" done < <(grep -E "^${imdb_id}[[:space:]]+" "${links_file}" | sort -u) else EchoW "No links.db file found at ${links_file}" fi output+="</ul>" output+="<h2>Bio</h2>" local bio_file="${MY_MDB_CACHE}/${imdb_id}_bio.db" if [[ -f "${bio_file}" ]]; then output+="$(< "${bio_file}")" else EchoW "No biography found at ${bio_file}" output+="<p><em>Biography not available.</em></p>" fi ACTOR_FILMOGRAPHY="${output}" } #============================================================================== # DoActor_0_Filmography: Generate and submit actor filmography to Blogger #------------------------------------------------------------------------------ # Description: # This function handles the full flow of building, previewing, or submitting # a Blogger blog post for an actor’s filmography. If a Blogger URL already # exists and update mode is not enabled, the user is prompted to: # (a) copy the HTML to clipboard, # (b) save it to a local HTML file, # (c) publish a new Blogger post and update the actor cache, or # (d) cancel the operation. # Otherwise, it regenerates and submits a new draft directly. # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor # ACTOR_QUERY - Name of the actor (used for fallback warning) # ACTOR_FILMOGRAPHY - HTML content generated by GenerateActorFilmographyFromTmdb # UPDATE_MODE - Set to 1 if forcing Blogger update # DEBUG - If non-zero, enable debug output # DRYRUN - If set, suppress actual blog submission # # Outputs: # - Echoes progress and user options # - Optionally writes filmography HTML to clipboard or file # - Submits a Blogger draft (unless DRYRUN is enabled) # - Updates actor.db with the Blogger URL if published # # Exit Codes: # 0 - Success # 1 - Failure or user cancellation #============================================================================== DoActor_0_Filmography() { local new_url local blogger_url Echo "Generating filmography for: ${actor_name}" (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: Using IMDb ID: ${ACTOR_IMDB_ID}" [[ -z "${ACTOR_IMDB_ID}" ]] && EchoE "Could not resolve IMDb ID for actor: ${actor_name}" && exit 1 blogger_url="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")" if [[ -n "${blogger_url}" && ${UPDATE_MODE} -eq 0 ]]; then while true; do Echo "Existing Blogger URL found: ${blogger_url}" Echo "Choose one of the following options:" Echo " a) Copy HTML to /dev/clipboard" Echo " b) Save to file: ${ACTOR_IMDB_ID}.html" Echo " c) Create a new Blogger entry and update cache" Echo " d) Cancel" read -rp "Your choice (a/b/c/d): " choice choice="${choice:0:1}" choice="${choice,,}" case "${choice,,}" in a) GenerateActorFilmographyFromTmdb [[ -z "${ACTOR_FILMOGRAPHY}" ]] && EchoE "Failed to generate filmography." && exit 1 echo "${ACTOR_FILMOGRAPHY}" > /dev/clipboard Echo "Copied HTML to /dev/clipboard." exit 0 ;; b) GenerateActorFilmographyFromTmdb [[ -z "${ACTOR_FILMOGRAPHY}" ]] && EchoE "Failed to generate filmography." && exit 1 echo "${ACTOR_FILMOGRAPHY}" > "${ACTOR_IMDB_ID}.html" Echo "Saved HTML to ${ACTOR_IMDB_ID}.html" exit 0 ;; c) GenerateActorFilmographyFromTmdb [[ -z "${ACTOR_FILMOGRAPHY}" ]] && EchoE "Failed to generate filmography." && exit 1 new_url="$(SubmitBlogDraft "${ACTOR_FILMOGRAPHY}")" Echo "🌐 Blogger URL: ${new_url}" DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_url#*blogspot.com/}" break ;; d) Echo "Aborted by user." exit 1 ;; esac EchoE "Invalid choice '${choice}'. Try again." done else # No existing Blogger post or in update mode GenerateActorFilmographyFromTmdb [[ -z "${ACTOR_FILMOGRAPHY}" ]] && EchoE "Failed to generate filmography." && exit 1 new_url="$(SubmitBlogDraft "${ACTOR_FILMOGRAPHY}")" Echo "🌐 Blogger URL: ${new_url}" DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_url#*blogspot.com/}" fi Echo "✅ Done: Post created or updated for IMDb ID '${ACTOR_IMDB_ID}'." } #============================================================================== # DoActor_1_ShowUrl: Display the current Blogger URL for a given actor #------------------------------------------------------------------------------ # Description: # This function queries the actor.db cache to retrieve the Blogger URL # associated with the IMDb ID stored in ACTOR_IMDB_ID. It outputs the URL # directly, or a blank if no entry exists. # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor (used as lookup key) # ACTOR_DB - Full path to ~/.MyBlogCache/actor.db # # Outputs: # Echoes the Blogger URL to stdout if it exists, otherwise a blank URL notice. # # Exit Codes: # 0 - Always succeeds; informational only #============================================================================== DoActor_1_ShowUrl() { local blogger_url blogger_url="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")" if [[ -n "${blogger_url}" ]]; then Echo "Current Blogger URL is '${blogger_url}'" else Echo "Current Blogger URL is ''" fi } #============================================================================== # DoActor_2_DeleteUrl: Remove the Blogger URL associated with an actor #------------------------------------------------------------------------------ # Description: # This function clears the Blogger URL field for the actor identified by # ACTOR_IMDB_ID in the actor.db cache file. It effectively disassociates # the actor from any previously posted Blogger URL. # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor (used as lookup key) # ACTOR_DB - Full path to ~/.MyBlogCache/actor.db (used internally) # actor_name - Display name for echo output (optional, used for clarity) # # Outputs: # Echoes a confirmation message indicating the Blogger URL was cleared. # # Exit Codes: # 0 - Always succeeds; informational only #============================================================================== DoActor_2_DeleteUrl() { Echo "Clearing Blogger URL for '${actor_name}'..." DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "" } #============================================================================== # DoActor_3_UpdateUrl: Update Blogger URL for actor and validate uniqueness #------------------------------------------------------------------------------ # Description: # Updates the Blogger URL for the actor identified by ACTOR_IMDB_ID. # Ensures the new URL is valid, unique across all actors, and optionally # allows overwriting identical entries if FORCE is set. Logs all updates to # ~/.MyMDBCache/actor_url_changes.log for auditing. # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor (used as the key) # ACTOR_DB - Full path to actor.db (used to check uniqueness) # actor_name - Actor's name (used for logging/display) # FORCE - If non-zero, allows update even if path is unchanged # # Arguments: # ${1} - New full Blogger URL (must start with https:// and contain blogspot.com) # # Outputs: # - Confirms update via Echo # - Logs change to ~/.MyMDBCache/actor_url_changes.log # - Warns or errors on malformed or duplicate URLs # # Exit Codes: # 0 - Success or no change needed # 1 - Invalid URL or duplication detected #============================================================================== DoActor_3_UpdateUrl() { (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}: 1='${1}'" local new_url="${1}" if [[ -z "${new_url}" ]]; then EchoE "Missing Blogger URL for update." exit 1 fi if [[ "${new_url}" != https://*blogspot.com/* ]]; then EchoE "Only HTTPS blogspot URLs are allowed." exit 1 fi local new_path="${new_url#*blogspot.com/}" new_path="${new_path%%/}" # Trim trailing slashes if [[ "${new_path}" == "${new_url}" ]]; then EchoE "Invalid Blogger URL format: ${new_url}" exit 1 fi if [[ "${new_path}" =~ [[:space:]] ]]; then EchoW "Blogger path contains spaces; this may indicate a malformed URL." fi local current_path current_path="$(DBMovie_GetActorBloggerUrl "${ACTOR_IMDB_ID}")" current_path="${current_path%%/}" if [[ "${new_path,,}" == "${current_path,,}" ]]; then if (( FORCE )); then EchoD "FORCE active: proceeding with update of identical path." else EchoW "New URL is same as current; skipping update..." return 0 fi fi # Check if another actor is using this path local path_owner path_owner="$(grep -i "|${new_path}$" "${ACTOR_DB}" | awk -F'|' '$1 != "'${ACTOR_IMDB_ID}'" {print $3}')" if [[ -n "${path_owner}" ]]; then EchoE "Another actor already uses this Blogger path: ${path_owner}" exit 1 fi Echo "🌐 Updating Blogger URL for '${actor_name}'..." Echo " Previous: '${current_path}'" Echo " New : '${new_path}'" # Save history local log="${HOME}/.MyMDBCache/actor_url_changes.log" local timestamp timestamp="$(date '+%Y-%m-%d %H:%M:%S')" echo "${timestamp}|${ACTOR_IMDB_ID}|${actor_name}|${current_path}|${new_path}" >> "${log}" DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${new_path}" Echo "🌐 Current Blogger URL is '${new_path}'" } #============================================================================== # DoActor_4_RevertUrl: Revert Blogger URL for actor from history #------------------------------------------------------------------------------ # Description: # Reverts the Blogger URL associated with an actor (identified by ACTOR_IMDB_ID) # to a previous value using the actor URL change log at # ~/.MyMDBCache/actor_url_changes.log. The number of steps to revert can be # specified; default is 1 (revert to immediately previous URL). # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor (used to identify history lines) # HOME - Used to locate the change history log under ~/.MyMDBCache # # Arguments: # ${1} - (Optional) Number of steps to revert back (default is 1) # # Outputs: # - Echo message indicating new (reverted) Blogger URL # - Error messages if log file is missing, entry not found, or step is invalid # # Exit Codes: # 0 - Successful revert # 1 - Error: log file missing, insufficient history, or malformed log entry #============================================================================== DoActor_4_RevertUrl() { local -i steps="${1:-1}" local log="${HOME}/.MyMDBCache/actor_url_changes.log" local -i count=0 if [[ ! -f "${log}" ]]; then EchoE "No history log found." exit 1 fi local lines lines="$(grep "|${ACTOR_IMDB_ID}|" "${log}" | tac)" if [[ -z "${lines}" ]]; then EchoE "No history for this actor." exit 1 fi while IFS= read -r line; do (( count++ )) if (( count == steps )); then local prev_path prev_path="$(echo "${line}" | cut -d'|' -f4)" if [[ -z "${prev_path}" ]]; then EchoE "Previous URL path not found in history." exit 1 fi Echo "Rolling back to '${prev_path}'..." DBMovie_UpdateActorBloggerUrl "${ACTOR_IMDB_ID}" "${prev_path}" return 0 fi done < <(echo "${lines}") EchoE "Unable to revert: fewer than ${steps} history entries found." exit 1 } #============================================================================== # DoActor_5_ShowUrlHistory: Display change history for actor Blogger URLs #------------------------------------------------------------------------------ # Description: # Displays the timestamped history of Blogger URL changes for a specific actor # using the IMDb ID defined in ACTOR_IMDB_ID. It parses the # ~/.MyMDBCache/actor_url_changes.log file and presents each historical update # as a timestamped transition from old to new URL path. # # Globals Used: # ACTOR_IMDB_ID - IMDb ID of the actor (used to match log entries) # HOME - Used to resolve log path at ~/.MyMDBCache/ # # Outputs: # - Prints timestamp, previous URL path, and updated URL path for each entry # - EchoE warning if the log file is missing # # Exit Codes: # 0 - Success (entries printed or empty result) # 1 - Error: history log not found #============================================================================== DoActor_5_ShowUrlHistory() { local log="${HOME}/.MyMDBCache/actor_url_changes.log" if [[ ! -f "${log}" ]]; then EchoE "No history file found." exit 1 fi grep "|${ACTOR_IMDB_ID}|" "${log}" | while IFS='|' read -r ts imdb name old new; do printf "🕓 %s: %s → %s\n" "${ts}" "${old}" "${new}" done } #============================================================================== # DoActor: Executes actor-related operations based on submode #------------------------------------------------------------------------------ # Description: # Routes execution to one of the DoActor_* functions depending on the selected # EXECUTION_SUBMODE. This is the main dispatcher for actor-related operations, # including generating filmography HTML, showing or updating Blogger URLs, # reverting URL changes, and displaying change history. # # Globals Used: # EXECUTION_SUBMODE - Mode selector (0=create, 1=show URL, 2=delete, 3=update, 4=revert, 5=history) # ACTOR_QUERY - Name of the actor (may be overridden by IMDB_ID) # IMDB_ID - Optional IMDb ID of the actor (takes precedence over ACTOR_QUERY) # ACTOR_IMDB_ID - Set internally after resolving actor identity # NEW_BLOGGER_URL - Used in update mode to set new Blogger path # REVERT_STEPS - Used in revert mode to control rollback depth # DEBUG - Enables diagnostic logging # # Arguments: # ${1} - Submode to execute (0 = filmography, 1 = show, 2 = delete, 3 = update, 4 = revert, 5 = history) # ${2} - Optional argument depending on the submode (e.g., new URL or number of steps) # # Exit Codes: # 0 - Success # 1 - Failure (e.g., unknown mode, failed resolution, missing actor record) #============================================================================== DoActor() { (( DEBUG )) && EchoD "${FUNCNAME[0]}: 1='${1}' 2='${2}' 3='${3}'" local -r mode="${1:-0}" DBMovie_InitActorDb DBMovie_InitMovieDb DBMovie_InitMovieCreditsDb [[ -n "${ACTOR_QUERY}" && -n "${IMDB_ID}" ]] && ACTOR_QUERY="" local actor_name="${ACTOR_QUERY}" ACTOR_IMDB_ID="$(ResolveActorImdbId "${actor_name}")" [[ -z "${ACTOR_IMDB_ID}" ]] && EchoE "Could not resolve IMDb ID for actor: ${actor_name}" && exit 1 local record="$(DBMovie_LoadActorRecord "${ACTOR_IMDB_ID}")" [[ -z "${record}" ]] && EchoE "Actor not found in actor.db: ${actor_name}" && exit 1 case "${mode}" in 0) DoActor_0_Filmography "${2}" ;; 1) DoActor_1_ShowUrl "${2}" ;; 2) DoActor_2_DeleteUrl "${2}" ;; 3) DoActor_3_UpdateUrl "${2}" ;; 4) DoActor_4_RevertUrl "${2}" ;; 5) DoActor_5_ShowUrlHistory "${2}" ;; *) EchoE "${FUNCNAME[0]}: Invalid execution mode '${mode}'" ; exit 1 ;; esac return 0; } #============================================================================== # DoMovieList: Scans movie directories and posts updates to Blogger #------------------------------------------------------------------------------ # Description: # Determines the effective REFERENCE_DATE (either today's date or CUSTOM_DATE # passed via --date). Scans for updates to movie directories, filters results # by year if applicable, builds HTML tables and summaries, and submits a # Blogger draft post containing the update. # # Globals Used: # CUSTOM_DATE - Optional override for the reference date # FORCE_PUBLISH - If set, overrides date/year warnings # FORCE - Bypasses user confirmation prompt # FILTER_YEARS[] - Optional list of years to filter updates # REFERENCE_DATE - Set here based on CUSTOM_DATE or today # REFERENCE_DATE_EPOCH - Epoch timestamp for REFERENCE_DATE # TODAY_EPOCH - Epoch timestamp for REFERENCE_DATE + 1 day # year_map[] - Filled by ScanDirectories with year → updated movies # # Outputs: # Blogger draft post submission with updated titles and summary # # Exit Codes: # 0 - Success # 1 - Invalid date format or aborted by user #============================================================================== DoMovieList() { # Validate and calculate effective date if [[ -z "${CUSTOM_DATE}" ]]; then EFFECTIVE_DATE=$(date --date=REFERENCE_DATE +%Y-%m-%d) else if ! date --date="${CUSTOM_DATE}" &>/dev/null; then Echo "ERROR: Invalid date format for --date: ${CUSTOM_DATE}"; exit 1 fi CUSTOM_EPOCH=$(date --date="${CUSTOM_DATE} 00:00:00" +%s) TODAY_EPOCH=$(date --date="today 00:00:00" +%s) if (( CUSTOM_EPOCH >= TODAY_EPOCH )); then Echo "ERROR: Specified date must be before today."; exit 1 fi EFFECTIVE_DATE=$(date --date="${CUSTOM_DATE}" +%Y-%m-%d) fi readonly REFERENCE_DATE="${EFFECTIVE_DATE}" readonly REFERENCE_DATE_EPOCH=$(date --date="${EFFECTIVE_DATE} 00:00:00" +%s) readonly TODAY_EPOCH=$(date --date="${EFFECTIVE_DATE} 00:00:00 + 1 day" +%s) if (( FORCE_PUBLISH == 1 && FORCE == 0 )); then if (( FORCE_PUBLISH == 1 && FORCE == 0 && ${#FILTER_YEARS[@]} > 0 )); then EchoW "⚠️ WARNING: You specified --publish AND limited the year(s) with --year/--range." EchoW " The post will not reflect updates from all years. Publishing it could be misleading." EchoW " Use --force to suppress this warning." read -p "Do you still want to proceed? (y/n): " answer [[ ! "${answer}" =~ ^[Yy]$ ]] && { Echo "Aborting by user request."; exit 1; } fi fi GetBlogAccessToken ValidateTmdbKey ScanDirectories if [[ ${#year_map[@]} -eq 0 ]]; then Echo "No updated titles found for ${REFERENCE_DATE}." else html_output="$(BuildHtmlTable)$(BuildSummaryHtml)" SubmitBlogDraft "${html_output}" Echo "Done." fi } #============================================================================== # Main: Entry point of script. Parses arguments and dispatches mode logic #------------------------------------------------------------------------------ # Description: # Main dispatcher for the script. Parses all command-line arguments, validates # dependencies, and routes to the appropriate execution mode (movie or actor). # Supports multiple submodes for actor operations such as filmography posting, # URL management, and history reversion. # # Globals: # EXECUTION_MODE - Primary mode of operation (1=movie-list, 2=actor) # EXECUTION_SUBMODE - Actor submode (0–5) # NEW_BLOGGER_URL - Passed to actor submode 3 (update) # REVERT_STEPS - Passed to actor submode 4 (revert) # DRYRUN - If set, suppresses blog submission # # Arguments: # All command-line parameters passed to the script # # Exit Codes: # 0 - Success # 1 - Invalid mode or submode, or argument parsing failure #============================================================================== Main() { DoDependency ParseArguments "$@" case ${EXECUTION_MODE} in 1) RunMovieUpdate ;; # Movie list mode 2) case ${EXECUTION_SUBMODE} in # 0: list actor filmography # 1: show actor blogger url # 2: delete actor blogger url # 3: update actor blogger url # 4: revert blogger url # 5: list blogger url 0|1|2|5) DoActor ${EXECUTION_SUBMODE} ;; 3) DoActor ${EXECUTION_SUBMODE} "${NEW_BLOGGER_URL}" ;; 4) DoActor ${EXECUTION_SUBMODE} "${REVERT_STEPS:-1}" ;; *) EchoE "Invalid --actor execution mode ${EXECUTION_SUBMODE}" ; exit 1 ;; esac ;; *) EchoE "Invalid execution mode" ; exit 1 ;; esac } Main "$@" exit 0 # ----------------------------------------------------------------------------- # __CHATGPT_INSTRUCTIONS_BEGIN__ # # All Bash functions in this script must include a header block with the following format: # #============================================================================== # <function-name> - <short description of what the function does> # Globals: # <VARIABLE_NAME> - Description of its use (RO for read-only, RW for read-write) # ... # Arguments: # $1 - Description of the first positional argument # $2 - Description of the second positional argument (if applicable) # ... # Outputs: # <Clearly state what is written to stdout or stderr> # <Any global variables modified (e.g., sets ACTOR_FILMOGRAPHY)> #============================================================================== # # Guidelines: # - Always use `local` for function-local variables. # - Use `${...}` consistently for all variable references. # - Output user-facing messages using the Echo, EchoE, EchoW, EchoD functions. # - When writing a function that returns data via stdout, prefer using a dedicated file descriptor (e.g., `>&5`) to avoid collisions with logging. # - Each function should be self-contained and document every global variable it accesses or modifies. # - Always wrap long `jq` or `curl` commands in clean, readable syntax with fallbacks or clear error handling. # # Logging functions are predefined and must be used instead of raw `echo`. # Each automatically includes a timestamp, an icon, and a log-level prefix. # # Use the following functions consistently: # # - `Echo` → for normal informational output (✅ or 📌) # - `EchoW` → for warnings (⚠️ WARNING:) # - `EchoE` → for errors (❌ ERROR:) # - `EchoD` → for debug output (🛠 DEBUG:) # # Important: # - Do **NOT** prepend messages manually with "ERROR:", "WARNING:", or emojis — these are automatically included by the functions. # - Do **NOT** call `echo` directly for user-facing output unless it's data being piped, stored, or exported. # - All log messages should be clear, human-readable, and aligned with script actions. # # Examples (✅ correct): # Echo "✅ Submitted successfully." # icon can be supplied # EchoW "Actor already exists in the database." # EchoE "Could not resolve TMDb ID for IMDb ID '${id}'." # EchoD "Parsed movie title: ${title}" # # Examples (❌ incorrect): # EchoW "WARNING: Actor exists." # redundant prefix # EchoE "❌ ERROR: Failed to post." # message is duplicated # EchoD "2025-04-22 00:51:35| Parsed movie title: ${title}" # message havs two time stamps # # 1. **Purpose and Modes**: # - The script supports four modes: --delete, --recheck-error, --set-priority, and --manage-downloads. # - --delete: Deletes torrents matching patterns in TV_Organize_Patterns.txt. # - --recheck-error: Rechecks torrents in an "error" state for a specific qBittorrent instance. # - --set-priority: Sets high priority and force-starts TV torrents matching specific prefixes. # - --manage-downloads: Moves completed torrents to a final directory and ensures incomplete ones are in a temp directory. # # 2. **Key Variables**: # - PREFIXES: Array of TV show prefixes used for matching in --set-priority mode. # - QBT_INSTANCES: Associative array mapping instance names (tv, anime, etc.) to qBittorrent ports. # - TEMP_DIRS/FINAL_DIRS: Directory mappings for --manage-downloads mode. # - DRY_RUN: Flag to simulate actions (default: 1, enabled). # # 3. **Dependencies**: # - Requires curl, jq, date, and urlencode (with fallbacks for urlencode). # - Dependency checks are configurable via --check-deps, --check-deps-local, --check-deps-install. # # 4. **Logging and Debugging**: # - Use --debug (-D) to enable verbose output, including curl commands and intermediate data. # - Logs are written to /cygdrive/f/p_qBittorrent/.logs for reachability and failures. # # 5. **Error Handling**: # - The script includes reachability checks for qBittorrent with retries. # - Temporary files are cleaned up on exit (via trap). # - API responses are checked for failures. # # 6. **Potential Improvements**: # - Add support for more qBittorrent API endpoints (e.g., pausing/resuming torrents). # - Enhance pattern matching with regex options in --delete mode. # - Add a --force option to skip user prompts in --delete mode. # # 7. **Known Issues**: # - jq 1.7.1 has a bug causing assertion failures with complex expressions. Workarounds include simplifying jq usage or upgrading to a newer version. # - Large torrent lists may cause performance issues; consider batching API calls. # # __CHATGPT_INSTRUCTIONS_END__ # -----------------------------------------------------------------------------
Comments
Post a Comment