feat(user/rand_media_pl.sh):Make robust and parallelize
[BK-2020-03.git] / user / rand_media_pl.sh
index a9c981b33d6ed229f86a985d4cd71b161c33caf7..ecf26daa2e04ed56048ad4e01e3c815fc5c3f26d 100755 (executable)
@@ -1,30 +1,31 @@
 #!/usr/bin/env bash
 #!/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.
 #   - 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]
 # 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
 
 # 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() {
 
 # 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
     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." ;;
             [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() {
 
 # 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
     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 ")"
     # 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
 
 # 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
     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
             # 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() {
 
 # 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
     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"
     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
 
 # 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
     else
-        generate_playlist_cache;
-        read_playlist_cache;
-    fi;
+        generate_playlist_cache
+        read_playlist_cache
+    fi
 else
 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
 
 # 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
 
 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)
 
 # 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
 # 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"]}"
     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"
 
         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
 
         # 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
         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
     fi
-    accumulated_duration="$new_accumulated_duration";
+    accumulated_duration="$new_accumulated_duration"
+    accumulated_duration_int="$new_accumulated_duration_int"
 done
 
 done
 
-die "FATAL: Could not find file corresponding to random point.";
-
+die "FATAL: Could not find file corresponding to random point."