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)."
83 # Function to read the playlist cache
84 read_playlist_cache
() {
85 # Input: file_durations associative array
86 # total_duration scalar
87 yell
"STATUS: Reading playlist cache."
90 while IFS
='|' read -r file duration
; do
91 declare -p file duration
1>&2 # Debugging statement
93 # Validate the duration
94 if [[ -n "$duration" && "$duration" =~ ^
[0-9]+(\.
[0-9]+)?$
]]; then
95 file_durations
["$file"]="$duration"
96 total_duration
=$
(echo "$total_duration + $duration" |
bc)
98 yell
"WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
104 if [[ -f "$CACHE_FILE" ]]; then
105 if prompt_yes_no
"Playlist cache detected. Do you want to use it? (Faster)"; then
108 generate_playlist_cache
112 generate_playlist_cache
116 # Check if any files were found
117 if [[ ${#file_durations[@]} -eq 0 ]]; then
118 declare -p file_durations
1>&2
119 die
"FATAL: No media files found."
122 # Ensure total_duration is not empty
123 if [[ -z "$total_duration" ]]; then
124 die
"FATAL: total_duration is empty."
127 # Convert total_duration to an integer
128 total_duration_int
=$
(printf "%.0f" "$total_duration")
130 # Check if total_duration_int is a valid integer
131 if ! [[ "$total_duration_int" =~ ^
[0-9]+$
]]; then
132 die
"FATAL: total_duration_int is not a valid integer."
135 # Generate a random integer between 0 and total_duration_int - 1
136 random_point
=$
(shuf
-i 0-$
((total_duration_int
- 1)) -n1)
137 yell
"DEBUG: total_duration=$total_duration, total_duration_int=$total_duration_int, random_point=$random_point" 1>&2
139 # Get sorted list of files
140 mapfile
-t sorted_files
< <(printf '%s\n' "${!file_durations[@]}" |
sort)
142 # Find the file and offset corresponding to the random starting point
143 accumulated_duration
=0
144 accumulated_duration_int
=0
146 for idx
in "${!sorted_files[@]}"; do
147 file="${sorted_files[$idx]}"
148 duration
="${file_durations["$file"]}"
149 declare -p idx
file duration accumulated_duration
1>&2 # Debugging statement
151 # Validate the duration
152 if [[ -n "$duration" && "$duration" =~ ^
[0-9]+(\.
[0-9]+)?$
]]; then
153 new_accumulated_duration
=$
(echo "$accumulated_duration + $duration" |
bc)
154 # Convert accumulated durations to integers for comparison
155 accumulated_duration_int
=$
(printf "%.0f" "$accumulated_duration")
156 new_accumulated_duration_int
=$
(printf "%.0f" "$new_accumulated_duration")
158 yell
"WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
162 if (( random_point
< new_accumulated_duration_int
)); then
163 offset
=$
(echo "$random_point - $accumulated_duration_int" |
bc)
164 yell
"Starting playback from $offset seconds into file: $file"
168 echo "# mpv EDL v0" > "$edl_file"
170 # First line: file with offset
171 printf '%s,%s\n' "$file" "$offset" >> "$edl_file"
173 # Append the rest of the files
174 declare -p file offset idx sorted_files edl_file
1>&2; # debug
175 for (( i
=idx
+1; i
<${#sorted_files[@]}; i
++ )); do
176 next_file
="${sorted_files[$i]}"
177 yell
"STATUS:Adding:$next_file";
178 printf '%s\n' "$next_file" >> "$edl_file"
181 # Start playback using mpv
184 # Securely delete the EDL file
185 if [[ -n "$edl_file" && -f "$edl_file" ]]; then
189 die
"FATAL: edl_file is either unset or not a valid file, skipping deletion."
192 accumulated_duration
="$new_accumulated_duration"
193 accumulated_duration_int
="$new_accumulated_duration_int"
196 die
"FATAL: Could not find file corresponding to random point."