X-Git-Url: https://zdv2.bktei.com/gitweb/BK-2020-03.git/blobdiff_plain/b0f274d1f2b18d7d18ca86cbe612b3e37d2a7cdc..f059459690d6487577943a708737b386ff5b01ca:/user/rand_media_pl.sh diff --git a/user/rand_media_pl.sh b/user/rand_media_pl.sh index a9c981b..abe7766 100755 --- a/user/rand_media_pl.sh +++ b/user/rand_media_pl.sh @@ -1,20 +1,21 @@ #!/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.1.0 +# Dependencies: Bash 4, ffprobe, mpv, bc, shuf, GNU Parallel -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"; +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"; @@ -24,11 +25,11 @@ declare total_duration; # Function to prompt the user prompt_yes_no() { - yell "STATUS:User input required."; - local prompt_message="$1" - local user_input + yell "STATUS: User input required."; + local prompt_message="$1"; + local user_input; while true; do - read -rp "$prompt_message [y/n]: " user_input + read -rp "$prompt_message [y/n]: " user_input; case "$user_input" in [Yy]*) return 0 ;; [Nn]*) return 1 ;; @@ -39,14 +40,12 @@ prompt_yes_no() { # Function to find files with specified extensions find_media_files() { - yell "STATUS:Finding 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 for ext in "${EXTENSIONS[@]}"; do find_cmd+=("-iname" "$ext" "-o"); - #declare -p find_cmd 1>&2; # debug done; - # Remove the last "-o" and add ")" + # Remove the last "-o" and add ")"; unset 'find_cmd[-1]'; find_cmd+=(")"); "${find_cmd[@]}"; @@ -56,39 +55,71 @@ find_media_files() { 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; - - total_duration=0 + : > "$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); + else + yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2; 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.)"; -}; + done < <(find_media_files | parallel readlink -f '{}' | sort -u; ); + + 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) - done < "$CACHE_FILE" + 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"; +}; +# Function to get byte count of file name +prepend_path_bc() { + # Desc: Prepends a file path with %[path bytecount]% + # Usage: prepend_filename_bc [FILE] + # Input: arg1 str file path + # Output: stdout str %[int path length]%path + # Example: 'foo.txt' yields '%7%foo.txt' + # Depends: GNU Coreutils 8.32 (for 'wc') + # BK-2020-03: yell(), die(), must() + # Ref/Attrib: See “Syntax of mpv EDL files” https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst#syntax-of-mpv-edl-files + fin="$1"; + + if [[ ! -f "$fin" ]]; then + yell "WARNING:File does not exist:${fin}"; + fi; + + bytecount="$(printf "%s" "$fin" | wc -c; )"; + re='[0-9]'; + if [[ ! "$bytecount" =~ $re ]]; then + die "FATAL:Not an int:${bytecount}; $(declare -p fin bytecount)"; + fi; + printf "%%%s%%%s" "$bytecount" "$fin"; }; # Main script @@ -107,41 +138,84 @@ 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."; -fi + die "FATAL: No media files found."; +fi; -# Get sorted list of files -mapfile -t sorted_files < <(printf '%s\n' "${!file_durations[@]}" | sort) +# Ensure total_duration is not empty +if [[ -z "$total_duration" ]]; then + die "FATAL: total_duration is empty."; +fi; -# Generate a random starting point within the total duration -random_point=$(awk -v max="$total_duration" 'BEGIN{srand(); print rand()*max}') +# 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; ); # Find the file and offset corresponding to the random starting point -accumulated_duration=0 -for file in "${sorted_files[@]}"; do - 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) - yell "Starting playback from $offset seconds into file: $file" - - # Create a playlist file - playlist_file=$(mktemp) - printf '%s\n' "${sorted_files[@]}" > "$playlist_file" +accumulated_duration=0; +accumulated_duration_int=0; + +for idx in "${!sorted_files[@]}"; do + file="${sorted_files[$idx]}"; + duration="${file_durations["$file"]}"; + 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 an EDL file + edl_file=$(mktemp); + yell "DEBUG:EDL file at:${edl_file}"; # debug + echo "# mpv EDL v0" > "$edl_file"; + + # Add first file to start playback at random offset position + file_bc="$(prepend_path_bc "$file")"; # See https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst#syntax-of-mpv-edl-files + printf '%s,%s\n' "$file_bc" "$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]}"; + next_file_bc="$(prepend_path_bc "$next_file")"; + yell "STATUS:Adding:$next_file"; + printf '%s\n' "$next_file_bc" >> "$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" || exit 1; - # 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."; + die "FATAL: edl_file is either unset or not a valid file, skipping deletion."; fi; - fi + fi; accumulated_duration="$new_accumulated_duration"; -done + accumulated_duration_int="$new_accumulated_duration_int"; +done; die "FATAL: Could not find file corresponding to random point."; -