feat(user/rand_media_pl.sh):Make robust and parallelize
[BK-2020-03.git] / user / rand_media_pl.sh
... / ...
CommitLineData
1#!/usr/bin/env bash
2# Description:
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]
10# Version: 0.0.4
11# Dependencies: Bash 4, ffprobe, mpv, bc, shuf
12
13yell() { echo "$0: $*" >&2; } # Print script path and all args to stderr
14die() { yell "$*"; exit 111; } # Same as yell() but exits with code 111
15must() { "$@" || die "cannot $*"; } # Runs args as command, reports args if command fails
16
17# Configurable variables
18SEARCH_DIR="${1:-.}" # Default to current directory if no argument is provided
19MAX_DEPTH=8
20EXTENSIONS=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm")
21CACHE_FILE=".playlist_cache"
22
23declare -gA file_durations
24declare total_duration
25
26# Function to prompt the user
27prompt_yes_no() {
28 yell "STATUS: User input required."
29 local prompt_message="$1"
30 local user_input
31 while true; do
32 read -rp "$prompt_message [y/n]: " user_input
33 case "$user_input" in
34 [Yy]*) return 0 ;;
35 [Nn]*) return 1 ;;
36 *) echo "Please answer yes or no." ;;
37 esac
38 done
39}
40
41# Function to find files with specified extensions
42find_media_files() {
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")
47 done
48 # Remove the last "-o" and add ")"
49 unset 'find_cmd[-1]'
50 find_cmd+=(")")
51 "${find_cmd[@]}"
52}
53
54# Function to generate the playlist cache
55generate_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
59
60 total_duration=0
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
66
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)
72 else
73 yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
74 fi
75 done < <(find_media_files | sort)
76
77 total_duration_s=$(printf "%.1f" "$total_duration")
78 total_duration_h=$(echo "scale=1; $total_duration / 3600" | bc -l)
79
80 yell "Total duration of playlist: ${total_duration_s} seconds (${total_duration_h} hours)."
81}
82
83# Function to read the playlist cache
84read_playlist_cache() {
85 # Input: file_durations associative array
86 # total_duration scalar
87 yell "STATUS: Reading playlist cache."
88
89 total_duration=0
90 while IFS='|' read -r file duration; do
91 declare -p file duration 1>&2 # Debugging statement
92
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)
97 else
98 yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
99 fi
100 done < "$CACHE_FILE"
101}
102
103# Main script
104if [[ -f "$CACHE_FILE" ]]; then
105 if prompt_yes_no "Playlist cache detected. Do you want to use it? (Faster)"; then
106 read_playlist_cache
107 else
108 generate_playlist_cache
109 read_playlist_cache
110 fi
111else
112 generate_playlist_cache
113 read_playlist_cache
114fi
115
116# Check if any files were found
117if [[ ${#file_durations[@]} -eq 0 ]]; then
118 declare -p file_durations 1>&2
119 die "FATAL: No media files found."
120fi
121
122# Ensure total_duration is not empty
123if [[ -z "$total_duration" ]]; then
124 die "FATAL: total_duration is empty."
125fi
126
127# Convert total_duration to an integer
128total_duration_int=$(printf "%.0f" "$total_duration")
129
130# Check if total_duration_int is a valid integer
131if ! [[ "$total_duration_int" =~ ^[0-9]+$ ]]; then
132 die "FATAL: total_duration_int is not a valid integer."
133fi
134
135# Generate a random integer between 0 and total_duration_int - 1
136random_point=$(shuf -i 0-$((total_duration_int - 1)) -n1)
137yell "DEBUG: total_duration=$total_duration, total_duration_int=$total_duration_int, random_point=$random_point" 1>&2
138
139# Get sorted list of files
140mapfile -t sorted_files < <(printf '%s\n' "${!file_durations[@]}" | sort)
141
142# Find the file and offset corresponding to the random starting point
143accumulated_duration=0
144accumulated_duration_int=0
145
146for 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
150
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")
157 else
158 yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
159 continue
160 fi
161
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"
165
166 # Create an EDL file
167 edl_file=$(mktemp)
168 echo "# mpv EDL v0" > "$edl_file"
169
170 # First line: file with offset
171 printf '%s,%s\n' "$file" "$offset" >> "$edl_file"
172
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"
179 done
180
181 # Start playback using mpv
182 mpv "$edl_file"
183
184 # Securely delete the EDL file
185 if [[ -n "$edl_file" && -f "$edl_file" ]]; then
186 rm "$edl_file"
187 exit 0
188 else
189 die "FATAL: edl_file is either unset or not a valid file, skipping deletion."
190 fi
191 fi
192 accumulated_duration="$new_accumulated_duration"
193 accumulated_duration_int="$new_accumulated_duration_int"
194done
195
196die "FATAL: Could not find file corresponding to random point."