3 #   - Finds audio and video files with specified extensions up to a max depth of 8. 
   4 #   - Uses ffprobe to measure their durations. 
   5 #   - Creates a playlist starting at a random location within the total runtime. 
   6 #   - Stores durations and file paths in a dotfile for faster subsequent runs. 
   7 #   - Prompts the user whether to use the cached data or regenerate it. 
   8 #   - Uses an EDL file to specify the starting offset for the first file. 
   9 # Usage: rand_media_pl.sh [DIR] 
  11 # Dependencies: Bash 4, ffprobe, mpv, bc, shuf, GNU Parallel 
  13 yell
() { echo "$0: $*" >&2; } # Print script path and all args to stderr 
  14 die
() { yell 
"$*"; exit 111; } # Same as yell() but exits with code 111 
  15 must
() { "$@" || die 
"cannot $*"; } # Runs args as command, reports args if command fails 
  17 # Configurable variables 
  18 SEARCH_DIR
="${1:-.}"  # Default to current directory if no argument is provided 
  20 EXTENSIONS
=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm"); 
  21 CACHE_FILE
=".playlist_cache"; 
  23 declare -gA file_durations
; 
  24 declare total_duration
; 
  26 # Function to prompt the user 
  28     yell 
"STATUS: User input required."; 
  29     local prompt_message
="$1"; 
  32         read -rp "$prompt_message [y/n]: " user_input
; 
  36             *) echo "Please answer yes or no." ;; 
  41 # Function to find files with specified extensions 
  43     yell 
"STATUS: Finding media files."; 
  44     local find_cmd
=("find" "-L" "$SEARCH_DIR" "-maxdepth" "$MAX_DEPTH" "-type" "f" "("); 
  45     for ext 
in "${EXTENSIONS[@]}"; do 
  46         find_cmd
+=("-iname" "$ext" "-o"); 
  48     # Remove the last "-o" and add ")"; 
  54 # Function to generate the playlist cache 
  55 generate_playlist_cache
() { 
  56     yell 
"Generating playlist cache. This may take a while..." 
  57     # Initialize or empty the cache file 
  58     : > "$CACHE_FILE";  # Truncate or create the cache file 
  61     while IFS
= read -r file; do 
  62         # Get the duration using ffprobe 
  63         duration
=$
(ffprobe 
-v error 
-show_entries format
=duration \
 
  64             -of default
=noprint_wrappers
=1:nokey
=1 "$file"); 
  65         declare -p file duration 
1>&2;  # Debugging statement 
  67         # Validate the duration 
  68         if [[ -n "$duration" && "$duration" =~ ^
[0-9]+(\.
[0-9]+)?$ 
]]; then 
  69             # Append the file path and duration to the cache file 
  70             printf "%s|%s\n" "$file" "$duration" >> "$CACHE_FILE"; 
  71             total_duration
=$
(echo "$total_duration + $duration" | 
bc); 
  73             yell 
"WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2; 
  75     done < <(find_media_files | parallel readlink 
-f '{}' | 
sort -u; ); 
  77     total_duration_s
="$(printf "%.1f
" "$total_duration"; )"; 
  78     total_duration_h
="$(echo "scale
=1; $total_duration / 3600" | bc -l; )"; 
  80     yell 
"Total duration of playlist: ${total_duration_s} seconds (${total_duration_h} hours)."; 
  82 # Function to read the playlist cache 
  83 read_playlist_cache
() { 
  84     # Input: file_durations associative array 
  85     #        total_duration scalar 
  86     yell 
"STATUS: Reading playlist cache." 
  89     while IFS
='|' read -r file duration
; do 
  90         declare -p file duration 
1>&2  # Debugging statement 
  92         # Validate the duration 
  93         if [[ -n "$duration" && "$duration" =~ ^
[0-9]+(\.
[0-9]+)?$ 
]]; then 
  94             file_durations
["$file"]="$duration" 
  95             total_duration
=$
(echo "$total_duration + $duration" | 
bc) 
  97             yell 
"WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2 
 101 # Function to get byte count of file name 
 103     # Desc: Prepends a file path with %[path bytecount]% 
 104     # Usage: prepend_filename_bc [FILE] 
 105     # Input:  arg1    str  file path 
 106     # Output: stdout  str  %[int path length]%path 
 107     # Example: 'foo.txt' yields '%7%foo.txt' 
 108     # Depends: GNU Coreutils 8.32 (for 'wc') 
 109     #          BK-2020-03: yell(), die(), must() 
 110     # 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 
 113     if [[ ! -f "$fin" ]]; then 
 114         yell 
"WARNING:File does not exist:${fin}"; 
 117     bytecount
="$(printf "%s
" "$fin" | wc -c; )"; 
 119     if [[ ! "$bytecount" =~ 
$re ]]; then 
 120         die 
"FATAL:Not an int:${bytecount}; $(declare -p fin bytecount)"; 
 122     printf "%%%s%%%s" "$bytecount" "$fin"; 
 126 if [[ -f "$CACHE_FILE" ]]; then 
 127     if prompt_yes_no 
"Playlist cache detected. Do you want to use it? (Faster)"; then 
 130         generate_playlist_cache
; 
 134     generate_playlist_cache
; 
 138 # Check if any files were found 
 139 if [[ ${#file_durations[@]} -eq 0 ]]; then 
 140     declare -p file_durations 
1>&2; 
 141     die 
"FATAL: No media files found."; 
 144 # Ensure total_duration is not empty 
 145 if [[ -z "$total_duration" ]]; then 
 146     die 
"FATAL: total_duration is empty."; 
 149 # Convert total_duration to an integer 
 150 total_duration_int
=$
(printf "%.0f" "$total_duration"); 
 152 # Check if total_duration_int is a valid integer 
 153 if ! [[ "$total_duration_int" =~ ^
[0-9]+$ 
]]; then 
 154     die 
"FATAL: total_duration_int is not a valid integer."; 
 157 # Generate a random integer between 0 and total_duration_int - 1 
 158 random_point
=$
(shuf 
-i 0-$
((total_duration_int 
- 1)) -n1) 
 159 yell 
"DEBUG: total_duration=$total_duration, total_duration_int=$total_duration_int, random_point=$random_point" 1>&2; 
 161 # Get sorted list of files 
 162 mapfile 
-t sorted_files 
< <(printf '%s\n' "${!file_durations[@]}" | 
sort; ); 
 164 # Find the file and offset corresponding to the random starting point 
 165 accumulated_duration
=0; 
 166 accumulated_duration_int
=0; 
 168 for idx 
in "${!sorted_files[@]}"; do 
 169     file="${sorted_files[$idx]}"; 
 170     duration
="${file_durations["$file"]}"; 
 171     declare -p idx 
file duration accumulated_duration 
1>&2;  # Debugging statement 
 173     # Validate the duration 
 174     if [[ -n "$duration" && "$duration" =~ ^
[0-9]+(\.
[0-9]+)?$ 
]]; then 
 175         new_accumulated_duration
=$
(echo "$accumulated_duration + $duration" | 
bc) 
 176         # Convert accumulated durations to integers for comparison 
 177         accumulated_duration_int
=$
(printf "%.0f" "$accumulated_duration") 
 178         new_accumulated_duration_int
=$
(printf "%.0f" "$new_accumulated_duration") 
 180         yell 
"WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2 
 184     if (( random_point 
< new_accumulated_duration_int 
)); then 
 185         offset
=$
(echo "$random_point - $accumulated_duration_int" | 
bc; ); 
 186         yell 
"Starting playback from $offset seconds into file: $file"; 
 190         yell 
"DEBUG:EDL file at:${edl_file}"; # debug 
 191         echo "# mpv EDL v0" > "$edl_file"; 
 193         # Add first file to start playback at random offset position 
 194         file_bc
="$(prepend_path_bc "$file")"; # See https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst#syntax-of-mpv-edl-files 
 195         printf '%s,%s\n' "$file_bc" "$offset" >> "$edl_file"; 
 197         # Append the rest of the files 
 198         declare -p file offset idx sorted_files edl_file 
1>&2; # debug 
 199         for (( i
=idx
+1; i
<${#sorted_files[@]}; i
++ )); do 
 200             next_file
="${sorted_files[$i]}"; 
 201             next_file_bc
="$(prepend_path_bc "$next_file")"; 
 202             yell 
"STATUS:Adding:$next_file"; 
 203             printf '%s\n' "$next_file_bc" >> "$edl_file"; 
 206         # Start playback using mpv 
 207         mpv 
"$edl_file" || 
exit 1; 
 209         # Securely delete the EDL file 
 210         if [[ -n "$edl_file" && -f "$edl_file" ]]; then 
 214             die 
"FATAL: edl_file is either unset or not a valid file, skipping deletion."; 
 217     accumulated_duration
="$new_accumulated_duration"; 
 218     accumulated_duration_int
="$new_accumulated_duration_int"; 
 221 die 
"FATAL: Could not find file corresponding to random point.";