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
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 |
sort; );
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.";