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 # Usage: rand_media_pl.sh [DIR]
10 # Dependencies: Bash 4, ffprobe, mpv
12 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr
13 die
() { yell
"$*"; exit 111; } # same as yell() but non-zero exit status
14 must
() { "$@" || die
"cannot $*"; } # runs args as command, reports args if command fails
16 # Configurable variables
19 EXTENSIONS
=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm");
20 CACHE_FILE
=".playlist_cache";
22 declare -gA file_durations
;
23 declare total_duration
;
25 # Function to prompt the user
27 yell
"STATUS:User input required.";
28 local prompt_message
="$1"
31 read -rp "$prompt_message [y/n]: " user_input
35 *) echo "Please answer yes or no." ;;
40 # Function to find files with specified extensions
42 yell
"STATUS:Finding media files.";
43 local find_cmd
=("find" "-L" "$SEARCH_DIR" "-maxdepth" "$MAX_DEPTH" "-type" "f" "(");
44 #declare -p find_cmd 1>&2; # debug
45 for ext
in "${EXTENSIONS[@]}"; do
46 find_cmd
+=("-iname" "$ext" "-o");
47 #declare -p find_cmd 1>&2; # debug
49 # Remove the last "-o" and add ")"
55 # Function to generate the playlist cache
56 generate_playlist_cache
() {
57 yell
"Generating playlist cache. This may take a while..."
58 # Initialize or empty the cache file
59 if [[ ! -f "$CACHE_FILE" ]]; then
66 while IFS
= read -r file; do
67 # Get the duration using ffprobe
68 duration
=$
(ffprobe
-v error
-show_entries format
=duration
-of default
=noprint_wrappers
=1:nokey
=1 "$file")
69 if [[ -n "$duration" ]]; then
70 # Append the file path and duration to the cache file
71 printf "%s|%s\n" "$file" "$duration" >> "$CACHE_FILE";
72 total_duration
=$
(echo "$total_duration + $duration" |
bc);
74 done < <(find_media_files |
sort);
75 total_duration_s
="$(echo "scale
=1; ${total_duration} / 1" | bc -l)";
76 total_duration_h
="$(echo "scale
=1; ${total_duration} / 3600" | bc -l)";
78 yell
"Total duration of playlist: ${total_duration_s} seconds. (${total_duration_h} hours.)";
81 # Function to read the playlist cache
82 read_playlist_cache
() {
83 # Input: file_durations assoc. array
84 # total_duration array
85 yell
"STATUS:Reading playlist cache.";
88 while IFS
='|' read -r file duration
; do
89 file_durations
["$file"]="$duration"
90 total_duration
=$
(echo "$total_duration + $duration" |
bc)
95 if [[ -f "$CACHE_FILE" ]]; then
96 if prompt_yes_no
"Playlist cache detected. Do you want to use it? (Faster)"; then
99 generate_playlist_cache
;
103 generate_playlist_cache
;
107 # Check if any files were found
108 if [[ ${#file_durations[@]} -eq 0 ]]; then
109 declare -p file_durations
1>&2;
110 die
"FATAL:No media files found.";
113 # Get sorted list of files
114 mapfile
-t sorted_files
< <(printf '%s\n' "${!file_durations[@]}" |
sort)
116 # Generate a random starting point within the total duration
117 random_point
=$
(awk -v max
="$total_duration" 'BEGIN{srand(); print rand()*max}')
119 # Find the file and offset corresponding to the random starting point
120 accumulated_duration
=0
121 for file in "${sorted_files[@]}"; do
122 duration
="${file_durations["$file"]}"
123 new_accumulated_duration
=$
(echo "$accumulated_duration + $duration" |
bc)
124 if (( $
(echo "$random_point < $new_accumulated_duration" |
bc -l) )); then
125 offset
=$
(echo "$random_point - $accumulated_duration" |
bc)
126 yell
"Starting playback from $offset seconds into file: $file"
128 # Create a playlist file
129 playlist_file
=$
(mktemp
)
130 printf '%s\n' "${sorted_files[@]}" > "$playlist_file"
132 # Start playback using mpv
133 mpv
--playlist="$playlist_file" --start="$offset" \
134 --playlist-start=$
(($
(printf '%s\n' "${sorted_files[@]}" |
grep -n "^$file$" | cut
-d: -f1)-1))
136 # Securely delete the playlist file
137 if [[ -n "$playlist_file" && -f "$playlist_file" ]]; then
138 rm "$playlist_file"; exit 0;
140 die
"FATAL: playlist_file is either unset or not a valid file, skipping deletion.";
143 accumulated_duration
="$new_accumulated_duration";
146 die
"FATAL: Could not find file corresponding to random point.";