#!/bin/bash ################################################################################ # Plex Library Scanner Script ################################################################################ # # Author: Peter Wood # Description: Command-line interface for Plex Media Scanner operations. # Provides library scanning, metadata refresh, and analysis # capabilities with styled output and error handling. # # Features: # - List all Plex libraries with section IDs # - Scan specific libraries or all libraries for new media # - Force refresh metadata for libraries # - Deep analysis of media files # - Generate thumbnails and preview images # - Interactive mode for easy library management # - Comprehensive error handling and validation # # Usage: # ./scan-plex-libraries.sh list # List all libraries # ./scan-plex-libraries.sh scan [section_id] # Scan library (all if no ID) # ./scan-plex-libraries.sh refresh [section_id] # Refresh library metadata # ./scan-plex-libraries.sh analyze [section_id] # Analyze library media # ./scan-plex-libraries.sh generate [section_id] # Generate thumbnails # ./scan-plex-libraries.sh # Interactive mode # # Dependencies: # - Plex Media Server # - Plex Media Scanner binary # - systemctl (for service management) # # Exit Codes: # 0 - Success # 1 - General error # 2 - Plex service not running # 3 - Scanner binary not found # 4 - Invalid section ID # 5 - Scanner operation failed # ################################################################################ set -euo pipefail # 🎨 Color definitions for styled output readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly PURPLE='\033[0;35m' readonly CYAN='\033[0;36m' readonly WHITE='\033[1;37m' readonly BOLD='\033[1m' readonly DIM='\033[2m' readonly RESET='\033[0m' # 🔧 Configuration readonly PLEX_SERVICE="plexmediaserver" readonly SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" readonly LOG_DIR="${SCRIPT_DIR}/../logs" readonly LOG_FILE="${LOG_DIR}/plex-scanner.log" # Plex Media Scanner paths (common locations) readonly SCANNER_PATHS=( "/usr/lib/plexmediaserver/Plex Media Scanner" "/opt/plex/Plex Media Scanner" "/usr/local/plex/Plex Media Scanner" "/Applications/Plex Media Server.app/Contents/MacOS/Plex Media Scanner" ) # 🎭 ASCII symbols for compatible output readonly CHECKMARK="[✓]" readonly CROSS="[✗]" readonly ROCKET="[>]" readonly STOP_SIGN="[■]" readonly RECYCLE="[~]" readonly INFO="[i]" readonly HOURGLASS="[*]" readonly SPARKLES="[*]" readonly SEARCH="[?]" readonly LIBRARY="[📚]" # Global variables SCANNER_PATH="" VERBOSE=false # 🎉 Function to print completion footer print_footer() { echo -e "\n${DIM}${CYAN}--- Operation completed ${SPARKLES} ---${RESET}\n" } # 🎯 Function to print status with style print_status() { local status="$1" local message="$2" local color="$3" echo -e "${color}${BOLD}[${status}]${RESET} ${message}" # Log to file if log directory exists if [[ -d "$LOG_DIR" ]]; then echo "$(date '+%Y-%m-%d %H:%M:%S') - [${status}] ${message}" >> "$LOG_FILE" fi } # 📝 Function to log verbose output log_verbose() { local message="$1" if [[ "$VERBOSE" == true ]]; then echo -e "${DIM}${CYAN}[DEBUG]${RESET} ${message}" fi # Always log to file if available if [[ -d "$LOG_DIR" ]]; then echo "$(date '+%Y-%m-%d %H:%M:%S') - [DEBUG] ${message}" >> "$LOG_FILE" fi } # 🔍 Function to find Plex Media Scanner binary find_scanner() { log_verbose "Searching for Plex Media Scanner binary..." for path in "${SCANNER_PATHS[@]}"; do if [[ -f "$path" && -x "$path" ]]; then SCANNER_PATH="$path" log_verbose "Found scanner at: $path" return 0 fi done # Try to find it in PATH if command -v "Plex Media Scanner" >/dev/null 2>&1; then SCANNER_PATH="Plex Media Scanner" log_verbose "Found scanner in PATH" return 0 fi print_status "${CROSS}" "Plex Media Scanner binary not found!" "${RED}" print_status "${INFO}" "Checked locations:" "${YELLOW}" for path in "${SCANNER_PATHS[@]}"; do echo -e " ${DIM}${YELLOW}• $path${RESET}" done return 1 } # 🏥 Function to check Plex service status check_plex_service() { log_verbose "Checking Plex service status..." if ! systemctl is-active --quiet "$PLEX_SERVICE"; then print_status "${CROSS}" "Plex Media Server is not running!" "${RED}" print_status "${INFO}" "Start Plex with: ${BOLD}sudo systemctl start $PLEX_SERVICE${RESET}" "${YELLOW}" return 1 fi log_verbose "Plex service is running" return 0 } # 📚 Function to list all library sections list_libraries() { print_status "${LIBRARY}" "Listing all Plex library sections..." "${BLUE}" if ! check_plex_service; then return 2 fi if ! find_scanner; then return 3 fi # Set library path for Linux export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} local output if output=$("$SCANNER_PATH" --list 2>&1); then echo "" echo -e "${BOLD}${CYAN}Available Library Sections:${RESET}" echo -e "${DIM}${CYAN}=========================${RESET}" # Parse and format the output echo "$output" | while IFS= read -r line; do if [[ "$line" =~ ^[[:space:]]*([0-9]+):[[:space:]]*(.+)$ ]]; then local section_id="${BASH_REMATCH[1]}" local section_name="${BASH_REMATCH[2]}" echo -e "${GREEN}${BOLD} ${section_id}:${RESET} ${WHITE}${section_name}${RESET}" elif [[ -n "$line" ]]; then echo -e "${DIM} $line${RESET}" fi done echo "" print_status "${CHECKMARK}" "Library listing completed" "${GREEN}" return 0 else print_status "${CROSS}" "Failed to list libraries" "${RED}" echo -e "${DIM}${RED}Error output: $output${RESET}" return 5 fi } # 🔍 Function to validate section ID validate_section_id() { local section_id="$1" if [[ ! "$section_id" =~ ^[0-9]+$ ]]; then print_status "${CROSS}" "Invalid section ID: $section_id (must be a number)" "${RED}" return 1 fi # Get list of valid section IDs export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} local valid_ids if valid_ids=$("$SCANNER_PATH" --list 2>/dev/null | grep -oE '^[[:space:]]*[0-9]+:' | grep -oE '[0-9]+'); then if echo "$valid_ids" | grep -q "^${section_id}$"; then return 0 else print_status "${CROSS}" "Section ID $section_id not found" "${RED}" print_status "${INFO}" "Valid section IDs: $(echo "$valid_ids" | tr '\n' ' ')" "${YELLOW}" return 1 fi else print_status "${CROSS}" "Unable to validate section ID" "${RED}" return 1 fi } # 🔄 Function to scan library for new media scan_library() { local section_id="$1" if [[ -n "$section_id" ]]; then print_status "${ROCKET}" "Scanning library section $section_id for new media..." "${BLUE}" if ! validate_section_id "$section_id"; then return 4 fi export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} if "$SCANNER_PATH" --scan --section "$section_id" ${VERBOSE:+--verbose}; then print_status "${CHECKMARK}" "Library section $section_id scan completed" "${GREEN}" return 0 else print_status "${CROSS}" "Failed to scan library section $section_id" "${RED}" return 5 fi else print_status "${ROCKET}" "Scanning all libraries for new media..." "${BLUE}" # Get all section IDs and scan each one export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} local section_ids if section_ids=$("$SCANNER_PATH" --list 2>/dev/null | grep -oE '^[[:space:]]*[0-9]+:' | grep -oE '[0-9]+'); then local failed_sections=() while IFS= read -r id; do [[ -n "$id" ]] || continue print_status "${INFO}" "Scanning section $id..." "${YELLOW}" if "$SCANNER_PATH" --scan --section "$id" ${VERBOSE:+--verbose}; then print_status "${CHECKMARK}" "Section $id scanned successfully" "${GREEN}" else print_status "${CROSS}" "Failed to scan section $id" "${RED}" failed_sections+=("$id") fi done <<< "$section_ids" if [[ ${#failed_sections[@]} -eq 0 ]]; then print_status "${CHECKMARK}" "All libraries scanned successfully" "${GREEN}" return 0 else print_status "${CROSS}" "Some libraries failed to scan: ${failed_sections[*]}" "${RED}" return 5 fi else print_status "${CROSS}" "Unable to retrieve library list" "${RED}" return 5 fi fi } # 🔄 Function to refresh library metadata refresh_library() { local section_id="$1" local force="${2:-false}" local force_flag="" if [[ "$force" == "true" ]]; then force_flag="--force" fi if [[ -n "$section_id" ]]; then print_status "${RECYCLE}" "Refreshing metadata for library section $section_id..." "${BLUE}" if ! validate_section_id "$section_id"; then return 4 fi export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} if "$SCANNER_PATH" --refresh $force_flag --section "$section_id" ${VERBOSE:+--verbose}; then print_status "${CHECKMARK}" "Library section $section_id metadata refreshed" "${GREEN}" return 0 else print_status "${CROSS}" "Failed to refresh library section $section_id" "${RED}" return 5 fi else print_status "${RECYCLE}" "Refreshing metadata for all libraries..." "${BLUE}" # Get all section IDs and refresh each one export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} local section_ids if section_ids=$("$SCANNER_PATH" --list 2>/dev/null | grep -oE '^[[:space:]]*[0-9]+:' | grep -oE '[0-9]+'); then local failed_sections=() while IFS= read -r id; do [[ -n "$id" ]] || continue print_status "${INFO}" "Refreshing section $id..." "${YELLOW}" if "$SCANNER_PATH" --refresh $force_flag --section "$id" ${VERBOSE:+--verbose}; then print_status "${CHECKMARK}" "Section $id refreshed successfully" "${GREEN}" else print_status "${CROSS}" "Failed to refresh section $id" "${RED}" failed_sections+=("$id") fi done <<< "$section_ids" if [[ ${#failed_sections[@]} -eq 0 ]]; then print_status "${CHECKMARK}" "All libraries refreshed successfully" "${GREEN}" return 0 else print_status "${CROSS}" "Some libraries failed to refresh: ${failed_sections[*]}" "${RED}" return 5 fi else print_status "${CROSS}" "Unable to retrieve library list" "${RED}" return 5 fi fi } # 🔬 Function to analyze library media analyze_library() { local section_id="$1" local deep="${2:-false}" local analyze_flag="--analyze" if [[ "$deep" == "true" ]]; then analyze_flag="--analyze-deeply" fi if [[ -n "$section_id" ]]; then print_status "${SEARCH}" "Analyzing media in library section $section_id..." "${BLUE}" if ! validate_section_id "$section_id"; then return 4 fi export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} if "$SCANNER_PATH" $analyze_flag --section "$section_id" ${VERBOSE:+--verbose}; then print_status "${CHECKMARK}" "Library section $section_id analysis completed" "${GREEN}" return 0 else print_status "${CROSS}" "Failed to analyze library section $section_id" "${RED}" return 5 fi else print_status "${SEARCH}" "Analyzing media in all libraries..." "${BLUE}" # Get all section IDs and analyze each one export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} local section_ids if section_ids=$("$SCANNER_PATH" --list 2>/dev/null | grep -oE '^[[:space:]]*[0-9]+:' | grep -oE '[0-9]+'); then local failed_sections=() while IFS= read -r id; do [[ -n "$id" ]] || continue print_status "${INFO}" "Analyzing section $id..." "${YELLOW}" if "$SCANNER_PATH" $analyze_flag --section "$id" ${VERBOSE:+--verbose}; then print_status "${CHECKMARK}" "Section $id analyzed successfully" "${GREEN}" else print_status "${CROSS}" "Failed to analyze section $id" "${RED}" failed_sections+=("$id") fi done <<< "$section_ids" if [[ ${#failed_sections[@]} -eq 0 ]]; then print_status "${CHECKMARK}" "All libraries analyzed successfully" "${GREEN}" return 0 else print_status "${CROSS}" "Some libraries failed to analyze: ${failed_sections[*]}" "${RED}" return 5 fi else print_status "${CROSS}" "Unable to retrieve library list" "${RED}" return 5 fi fi } # 🖼️ Function to generate thumbnails and fanart generate_thumbnails() { local section_id="$1" if [[ -n "$section_id" ]]; then print_status "${SPARKLES}" "Generating thumbnails for library section $section_id..." "${BLUE}" if ! validate_section_id "$section_id"; then return 4 fi export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} if "$SCANNER_PATH" --generate --section "$section_id" ${VERBOSE:+--verbose}; then print_status "${CHECKMARK}" "Thumbnails generated for library section $section_id" "${GREEN}" return 0 else print_status "${CROSS}" "Failed to generate thumbnails for library section $section_id" "${RED}" return 5 fi else print_status "${SPARKLES}" "Generating thumbnails for all libraries..." "${BLUE}" # Get all section IDs and generate thumbnails for each one export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} local section_ids if section_ids=$("$SCANNER_PATH" --list 2>/dev/null | grep -oE '^[[:space:]]*[0-9]+:' | grep -oE '[0-9]+'); then local failed_sections=() while IFS= read -r id; do [[ -n "$id" ]] || continue print_status "${INFO}" "Generating thumbnails for section $id..." "${YELLOW}" if "$SCANNER_PATH" --generate --section "$id" ${VERBOSE:+--verbose}; then print_status "${CHECKMARK}" "Section $id thumbnails generated successfully" "${GREEN}" else print_status "${CROSS}" "Failed to generate thumbnails for section $id" "${RED}" failed_sections+=("$id") fi done <<< "$section_ids" if [[ ${#failed_sections[@]} -eq 0 ]]; then print_status "${CHECKMARK}" "Thumbnails generated for all libraries" "${GREEN}" return 0 else print_status "${CROSS}" "Some libraries failed thumbnail generation: ${failed_sections[*]}" "${RED}" return 5 fi else print_status "${CROSS}" "Unable to retrieve library list" "${RED}" return 5 fi fi } # 🌳 Function to show library tree structure show_library_tree() { local section_id="$1" if [[ -z "$section_id" ]]; then print_status "${CROSS}" "Section ID required for tree display" "${RED}" return 1 fi print_status "${LIBRARY}" "Showing tree structure for library section $section_id..." "${BLUE}" if ! validate_section_id "$section_id"; then return 4 fi export LD_LIBRARY_PATH=/usr/lib/plexmediaserver:${LD_LIBRARY_PATH:-} if "$SCANNER_PATH" --tree --section "$section_id"; then print_status "${CHECKMARK}" "Tree display completed for library section $section_id" "${GREEN}" return 0 else print_status "${CROSS}" "Failed to display tree for library section $section_id" "${RED}" return 5 fi } # ❓ Function to show help show_help() { echo -e "${BOLD}${CYAN}Plex Library Scanner${RESET}" echo -e "${DIM}Modern command-line interface for Plex Media Scanner${RESET}" echo "" echo -e "${BOLD}Usage:${RESET}" echo -e " ${SCRIPT_NAME} ${GREEN}list${RESET} ${DIM}# List all library sections${RESET}" echo -e " ${SCRIPT_NAME} ${GREEN}scan${RESET} [section_id] ${DIM}# Scan for new media (all if no ID)${RESET}" echo -e " ${SCRIPT_NAME} ${GREEN}refresh${RESET} [section_id] [force] ${DIM}# Refresh metadata (force=true for forced refresh)${RESET}" echo -e " ${SCRIPT_NAME} ${GREEN}analyze${RESET} [section_id] [deep] ${DIM}# Analyze media (deep=true for deep analysis)${RESET}" echo -e " ${SCRIPT_NAME} ${GREEN}generate${RESET} [section_id] ${DIM}# Generate thumbnails and fanart${RESET}" echo -e " ${SCRIPT_NAME} ${GREEN}tree${RESET} ${DIM}# Show library tree structure${RESET}" echo -e " ${SCRIPT_NAME} ${GREEN}interactive${RESET} ${DIM}# Interactive mode${RESET}" echo "" echo -e "${BOLD}Options:${RESET}" echo -e " ${GREEN}-v, --verbose${RESET} ${DIM}# Enable verbose output${RESET}" echo -e " ${GREEN}-h, --help${RESET} ${DIM}# Show this help message${RESET}" echo "" echo -e "${BOLD}Examples:${RESET}" echo -e " ${DIM}# List all libraries${RESET}" echo -e " ${SCRIPT_NAME} list" echo "" echo -e " ${DIM}# Scan all libraries for new media${RESET}" echo -e " ${SCRIPT_NAME} scan" echo "" echo -e " ${DIM}# Scan specific library (ID 29)${RESET}" echo -e " ${SCRIPT_NAME} scan 29" echo "" echo -e " ${DIM}# Force refresh all libraries${RESET}" echo -e " ${SCRIPT_NAME} refresh \"\" true" echo "" echo -e " ${DIM}# Deep analyze specific library${RESET}" echo -e " ${SCRIPT_NAME} analyze 29 true" echo "" echo -e "${BOLD}Notes:${RESET}" echo -e " ${DIM}• Requires Plex Media Server to be running${RESET}" echo -e " ${DIM}• Operations may take considerable time for large libraries${RESET}" echo -e " ${DIM}• Use verbose mode (-v) for detailed progress information${RESET}" echo -e " ${DIM}• Log files are written to: ${LOG_FILE}${RESET}" } # 🎯 Interactive mode function interactive_mode() { echo -e "${BOLD}${CYAN}🎬 Plex Library Scanner - Interactive Mode${RESET}" echo -e "${DIM}Select an operation to perform:${RESET}" echo "" # First, check if Plex is running and scanner is available if ! check_plex_service; then return 2 fi if ! find_scanner; then return 3 fi while true; do echo -e "${BOLD}Available Operations:${RESET}" echo -e "${GREEN}1)${RESET} List all libraries" echo -e "${GREEN}2)${RESET} Scan libraries for new media" echo -e "${GREEN}3)${RESET} Refresh library metadata" echo -e "${GREEN}4)${RESET} Analyze library media" echo -e "${GREEN}5)${RESET} Generate thumbnails" echo -e "${GREEN}6)${RESET} Show library tree" echo -e "${GREEN}q)${RESET} Quit" echo "" read -p "$(echo -e "${BOLD}Choose an option [1-6,q]:${RESET} ")" choice case "$choice" in 1) echo "" list_libraries ;; 2) echo "" echo -e "${BOLD}Scan Options:${RESET}" echo -e "${GREEN}1)${RESET} Scan all libraries" echo -e "${GREEN}2)${RESET} Scan specific library" read -p "$(echo -e "${BOLD}Choose [1-2]:${RESET} ")" scan_choice case "$scan_choice" in 1) scan_library "" ;; 2) read -p "$(echo -e "${BOLD}Enter section ID:${RESET} ")" section_id scan_library "$section_id" ;; *) print_status "${CROSS}" "Invalid choice" "${RED}" ;; esac ;; 3) echo "" echo -e "${BOLD}Refresh Options:${RESET}" echo -e "${GREEN}1)${RESET} Refresh all libraries" echo -e "${GREEN}2)${RESET} Refresh specific library" echo -e "${GREEN}3)${RESET} Force refresh all libraries" echo -e "${GREEN}4)${RESET} Force refresh specific library" read -p "$(echo -e "${BOLD}Choose [1-4]:${RESET} ")" refresh_choice case "$refresh_choice" in 1) refresh_library "" false ;; 2) read -p "$(echo -e "${BOLD}Enter section ID:${RESET} ")" section_id refresh_library "$section_id" false ;; 3) refresh_library "" true ;; 4) read -p "$(echo -e "${BOLD}Enter section ID:${RESET} ")" section_id refresh_library "$section_id" true ;; *) print_status "${CROSS}" "Invalid choice" "${RED}" ;; esac ;; 4) echo "" echo -e "${BOLD}Analysis Options:${RESET}" echo -e "${GREEN}1)${RESET} Analyze all libraries" echo -e "${GREEN}2)${RESET} Analyze specific library" echo -e "${GREEN}3)${RESET} Deep analyze all libraries" echo -e "${GREEN}4)${RESET} Deep analyze specific library" read -p "$(echo -e "${BOLD}Choose [1-4]:${RESET} ")" analyze_choice case "$analyze_choice" in 1) analyze_library "" false ;; 2) read -p "$(echo -e "${BOLD}Enter section ID:${RESET} ")" section_id analyze_library "$section_id" false ;; 3) analyze_library "" true ;; 4) read -p "$(echo -e "${BOLD}Enter section ID:${RESET} ")" section_id analyze_library "$section_id" true ;; *) print_status "${CROSS}" "Invalid choice" "${RED}" ;; esac ;; 5) echo "" echo -e "${BOLD}Thumbnail Generation Options:${RESET}" echo -e "${GREEN}1)${RESET} Generate for all libraries" echo -e "${GREEN}2)${RESET} Generate for specific library" read -p "$(echo -e "${BOLD}Choose [1-2]:${RESET} ")" gen_choice case "$gen_choice" in 1) generate_thumbnails "" ;; 2) read -p "$(echo -e "${BOLD}Enter section ID:${RESET} ")" section_id generate_thumbnails "$section_id" ;; *) print_status "${CROSS}" "Invalid choice" "${RED}" ;; esac ;; 6) echo "" read -p "$(echo -e "${BOLD}Enter section ID to show tree:${RESET} ")" section_id show_library_tree "$section_id" ;; q|Q) print_status "${INFO}" "Goodbye!" "${CYAN}" break ;; *) print_status "${CROSS}" "Invalid choice: $choice" "${RED}" ;; esac echo "" echo -e "${DIM}Press Enter to continue...${RESET}" read -r echo "" done } # 🚀 Main script logic main() { # Create log directory if it doesn't exist mkdir -p "$LOG_DIR" # Check if running as root if [[ $EUID -eq 0 ]]; then print_status "${CROSS}" "Don't run this script as root! Use your regular user account." "${RED}" exit 1 fi # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in -v|--verbose) VERBOSE=true shift ;; -h|--help) show_help exit 0 ;; *) break ;; esac done # Check if no arguments provided or interactive requested if [[ $# -eq 0 ]] || [[ "${1,,}" == "interactive" ]]; then interactive_mode exit $? fi # Pre-flight checks if ! check_plex_service; then exit 2 fi if ! find_scanner; then exit 3 fi # Handle commands case "${1,,}" in "list") list_libraries ;; "scan") local section_id="${2:-}" scan_library "$section_id" ;; "refresh") local section_id="${2:-}" local force="${3:-false}" refresh_library "$section_id" "$force" ;; "analyze"|"analyse") local section_id="${2:-}" local deep="${3:-false}" analyze_library "$section_id" "$deep" ;; "generate"|"thumbnails") local section_id="${2:-}" generate_thumbnails "$section_id" ;; "tree") local section_id="$2" if [[ -z "$section_id" ]]; then print_status "${CROSS}" "Section ID required for tree command" "${RED}" exit 1 fi show_library_tree "$section_id" ;; "help"|"--help"|"-h") show_help ;; *) print_status "${CROSS}" "Unknown command: ${RED}${BOLD}$1${RESET}" "${RED}" echo "" show_help exit 1 ;; esac # Print footer for successful operations print_footer } # 🎬 Execute main function with all arguments main "$@"