From: Steven Baltakatei Sandoval Date: Sun, 22 Sep 2024 11:17:47 +0000 (+0000) Subject: feat(user/rand_media_pl.sh):Make robust and parallelize X-Git-Url: https://zdv2.bktei.com/gitweb/BK-2020-03.git/commitdiff_plain/00fcff36572aa4cb92fe5363c52d6ed346a20120?ds=inline;hp=b0f274d1f2b18d7d18ca86cbe612b3e37d2a7cdc feat(user/rand_media_pl.sh):Make robust and parallelize --- diff --git a/user/rand_media_pl.sh b/user/rand_media_pl.sh index a9c981b..ecf26da 100755 --- a/user/rand_media_pl.sh +++ b/user/rand_media_pl.sh @@ -1,30 +1,31 @@ #!/usr/bin/env bash -# Desc +# Description: # - Finds audio and video files with specified extensions up to a max depth of 8. # - Uses ffprobe to measure their durations. # - Creates a playlist starting at a random location within the total runtime. # - Stores durations and file paths in a dotfile for faster subsequent runs. # - Prompts the user whether to use the cached data or regenerate it. +# - Uses an EDL file to specify the starting offset for the first file. # Usage: rand_media_pl.sh [DIR] -# Version: 0.0.1 -# Dependencies: Bash 4, ffprobe, mpv +# Version: 0.0.4 +# Dependencies: Bash 4, ffprobe, mpv, bc, shuf -yell() { echo "$0: $*" >&2; } # print script path and all args to stderr -die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status -must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails +yell() { echo "$0: $*" >&2; } # Print script path and all args to stderr +die() { yell "$*"; exit 111; } # Same as yell() but exits with code 111 +must() { "$@" || die "cannot $*"; } # Runs args as command, reports args if command fails # Configurable variables -SEARCH_DIR="$1"; -MAX_DEPTH=8; -EXTENSIONS=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm"); -CACHE_FILE=".playlist_cache"; +SEARCH_DIR="${1:-.}" # Default to current directory if no argument is provided +MAX_DEPTH=8 +EXTENSIONS=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm") +CACHE_FILE=".playlist_cache" -declare -gA file_durations; -declare total_duration; +declare -gA file_durations +declare total_duration # Function to prompt the user prompt_yes_no() { - yell "STATUS:User input required."; + yell "STATUS: User input required." local prompt_message="$1" local user_input while true; do @@ -33,115 +34,163 @@ prompt_yes_no() { [Yy]*) return 0 ;; [Nn]*) return 1 ;; *) echo "Please answer yes or no." ;; - esac; - done; -}; + esac + done +} # Function to find files with specified extensions find_media_files() { - yell "STATUS:Finding media files."; - local find_cmd=("find" "-L" "$SEARCH_DIR" "-maxdepth" "$MAX_DEPTH" "-type" "f" "("); - #declare -p find_cmd 1>&2; # debug + yell "STATUS: Finding media files." + local find_cmd=("find" "-L" "$SEARCH_DIR" "-maxdepth" "$MAX_DEPTH" "-type" "f" "(") for ext in "${EXTENSIONS[@]}"; do - find_cmd+=("-iname" "$ext" "-o"); - #declare -p find_cmd 1>&2; # debug - done; + find_cmd+=("-iname" "$ext" "-o") + done # Remove the last "-o" and add ")" - unset 'find_cmd[-1]'; - find_cmd+=(")"); - "${find_cmd[@]}"; -}; + unset 'find_cmd[-1]' + find_cmd+=(")") + "${find_cmd[@]}" +} # Function to generate the playlist cache generate_playlist_cache() { yell "Generating playlist cache. This may take a while..." # Initialize or empty the cache file - if [[ ! -f "$CACHE_FILE" ]]; then - touch "$CACHE_FILE"; - else - rm "$CACHE_FILE"; - fi; - + : > "$CACHE_FILE" # Truncate or create the cache file + total_duration=0 while IFS= read -r file; do # Get the duration using ffprobe - duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file") - if [[ -n "$duration" ]]; then + duration=$(ffprobe -v error -show_entries format=duration \ + -of default=noprint_wrappers=1:nokey=1 "$file") + declare -p file duration 1>&2 # Debugging statement + + # Validate the duration + if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then # Append the file path and duration to the cache file - printf "%s|%s\n" "$file" "$duration" >> "$CACHE_FILE"; - total_duration=$(echo "$total_duration + $duration" | bc); - fi; - done < <(find_media_files | sort); - total_duration_s="$(echo "scale=1; ${total_duration} / 1" | bc -l)"; - total_duration_h="$(echo "scale=1; ${total_duration} / 3600" | bc -l)"; - - yell "Total duration of playlist: ${total_duration_s} seconds. (${total_duration_h} hours.)"; -}; + printf "%s|%s\n" "$file" "$duration" >> "$CACHE_FILE" + total_duration=$(echo "$total_duration + $duration" | bc) + else + yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2 + fi + done < <(find_media_files | sort) + + total_duration_s=$(printf "%.1f" "$total_duration") + total_duration_h=$(echo "scale=1; $total_duration / 3600" | bc -l) + + yell "Total duration of playlist: ${total_duration_s} seconds (${total_duration_h} hours)." +} # Function to read the playlist cache read_playlist_cache() { - # Input: file_durations assoc. array - # total_duration array - yell "STATUS:Reading playlist cache."; - + # Input: file_durations associative array + # total_duration scalar + yell "STATUS: Reading playlist cache." + total_duration=0 while IFS='|' read -r file duration; do - file_durations["$file"]="$duration" - total_duration=$(echo "$total_duration + $duration" | bc) + declare -p file duration 1>&2 # Debugging statement + + # Validate the duration + if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + file_durations["$file"]="$duration" + total_duration=$(echo "$total_duration + $duration" | bc) + else + yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2 + fi done < "$CACHE_FILE" -}; +} # Main script if [[ -f "$CACHE_FILE" ]]; then if prompt_yes_no "Playlist cache detected. Do you want to use it? (Faster)"; then - read_playlist_cache; + read_playlist_cache else - generate_playlist_cache; - read_playlist_cache; - fi; + generate_playlist_cache + read_playlist_cache + fi else - generate_playlist_cache; - read_playlist_cache; -fi; + generate_playlist_cache + read_playlist_cache +fi # Check if any files were found if [[ ${#file_durations[@]} -eq 0 ]]; then - declare -p file_durations 1>&2; - die "FATAL:No media files found."; + declare -p file_durations 1>&2 + die "FATAL: No media files found." +fi + +# Ensure total_duration is not empty +if [[ -z "$total_duration" ]]; then + die "FATAL: total_duration is empty." fi +# Convert total_duration to an integer +total_duration_int=$(printf "%.0f" "$total_duration") + +# Check if total_duration_int is a valid integer +if ! [[ "$total_duration_int" =~ ^[0-9]+$ ]]; then + die "FATAL: total_duration_int is not a valid integer." +fi + +# Generate a random integer between 0 and total_duration_int - 1 +random_point=$(shuf -i 0-$((total_duration_int - 1)) -n1) +yell "DEBUG: total_duration=$total_duration, total_duration_int=$total_duration_int, random_point=$random_point" 1>&2 + # Get sorted list of files mapfile -t sorted_files < <(printf '%s\n' "${!file_durations[@]}" | sort) -# Generate a random starting point within the total duration -random_point=$(awk -v max="$total_duration" 'BEGIN{srand(); print rand()*max}') - # Find the file and offset corresponding to the random starting point accumulated_duration=0 -for file in "${sorted_files[@]}"; do +accumulated_duration_int=0 + +for idx in "${!sorted_files[@]}"; do + file="${sorted_files[$idx]}" duration="${file_durations["$file"]}" - new_accumulated_duration=$(echo "$accumulated_duration + $duration" | bc) - if (( $(echo "$random_point < $new_accumulated_duration" | bc -l) )); then - offset=$(echo "$random_point - $accumulated_duration" | bc) + declare -p idx file duration accumulated_duration 1>&2 # Debugging statement + + # Validate the duration + if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + new_accumulated_duration=$(echo "$accumulated_duration + $duration" | bc) + # Convert accumulated durations to integers for comparison + accumulated_duration_int=$(printf "%.0f" "$accumulated_duration") + new_accumulated_duration_int=$(printf "%.0f" "$new_accumulated_duration") + else + yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2 + continue + fi + + if (( random_point < new_accumulated_duration_int )); then + offset=$(echo "$random_point - $accumulated_duration_int" | bc) yell "Starting playback from $offset seconds into file: $file" - # Create a playlist file - playlist_file=$(mktemp) - printf '%s\n' "${sorted_files[@]}" > "$playlist_file" + # Create an EDL file + edl_file=$(mktemp) + echo "# mpv EDL v0" > "$edl_file" + + # First line: file with offset + printf '%s,%s\n' "$file" "$offset" >> "$edl_file" + + # Append the rest of the files + declare -p file offset idx sorted_files edl_file 1>&2; # debug + for (( i=idx+1; i<${#sorted_files[@]}; i++ )); do + next_file="${sorted_files[$i]}" + yell "STATUS:Adding:$next_file"; + printf '%s\n' "$next_file" >> "$edl_file" + done # Start playback using mpv - mpv --playlist="$playlist_file" --start="$offset" \ - --playlist-start=$(($(printf '%s\n' "${sorted_files[@]}" | grep -n "^$file$" | cut -d: -f1)-1)) + mpv "$edl_file" - # Securely delete the playlist file - if [[ -n "$playlist_file" && -f "$playlist_file" ]]; then - rm "$playlist_file"; exit 0; + # Securely delete the EDL file + if [[ -n "$edl_file" && -f "$edl_file" ]]; then + rm "$edl_file" + exit 0 else - die "FATAL: playlist_file is either unset or not a valid file, skipping deletion."; - fi; + die "FATAL: edl_file is either unset or not a valid file, skipping deletion." + fi fi - accumulated_duration="$new_accumulated_duration"; + accumulated_duration="$new_accumulated_duration" + accumulated_duration_int="$new_accumulated_duration_int" done -die "FATAL: Could not find file corresponding to random point."; - +die "FATAL: Could not find file corresponding to random point."