feat(user/rand_media_pl.sh):Make robust and parallelize
[BK-2020-03.git] / user / rand_media_pl.sh
CommitLineData
b0f274d1 1#!/usr/bin/env bash
00fcff36 2# Description:
b0f274d1
SBS
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.
00fcff36 8# - Uses an EDL file to specify the starting offset for the first file.
b0f274d1 9# Usage: rand_media_pl.sh [DIR]
00fcff36
SBS
10# Version: 0.0.4
11# Dependencies: Bash 4, ffprobe, mpv, bc, shuf
b0f274d1 12
00fcff36
SBS
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
b0f274d1
SBS
16
17# Configurable variables
00fcff36
SBS
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"
b0f274d1 22
00fcff36
SBS
23declare -gA file_durations
24declare total_duration
b0f274d1
SBS
25
26# Function to prompt the user
27prompt_yes_no() {
00fcff36 28 yell "STATUS: User input required."
b0f274d1
SBS
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." ;;
00fcff36
SBS
37 esac
38 done
39}
b0f274d1
SBS
40
41# Function to find files with specified extensions
42find_media_files() {
00fcff36
SBS
43 yell "STATUS: Finding media files."
44 local find_cmd=("find" "-L" "$SEARCH_DIR" "-maxdepth" "$MAX_DEPTH" "-type" "f" "(")
b0f274d1 45 for ext in "${EXTENSIONS[@]}"; do
00fcff36
SBS
46 find_cmd+=("-iname" "$ext" "-o")
47 done
b0f274d1 48 # Remove the last "-o" and add ")"
00fcff36
SBS
49 unset 'find_cmd[-1]'
50 find_cmd+=(")")
51 "${find_cmd[@]}"
52}
b0f274d1
SBS
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
00fcff36
SBS
58 : > "$CACHE_FILE" # Truncate or create the cache file
59
b0f274d1
SBS
60 total_duration=0
61 while IFS= read -r file; do
62 # Get the duration using ffprobe
00fcff36
SBS
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
b0f274d1 69 # Append the file path and duration to the cache file
00fcff36
SBS
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}
b0f274d1
SBS
82
83# Function to read the playlist cache
84read_playlist_cache() {
00fcff36
SBS
85 # Input: file_durations associative array
86 # total_duration scalar
87 yell "STATUS: Reading playlist cache."
88
b0f274d1
SBS
89 total_duration=0
90 while IFS='|' read -r file duration; do
00fcff36
SBS
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
b0f274d1 100 done < "$CACHE_FILE"
00fcff36 101}
b0f274d1
SBS
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
00fcff36 106 read_playlist_cache
b0f274d1 107 else
00fcff36
SBS
108 generate_playlist_cache
109 read_playlist_cache
110 fi
b0f274d1 111else
00fcff36
SBS
112 generate_playlist_cache
113 read_playlist_cache
114fi
b0f274d1
SBS
115
116# Check if any files were found
117if [[ ${#file_durations[@]} -eq 0 ]]; then
00fcff36
SBS
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."
b0f274d1
SBS
125fi
126
00fcff36
SBS
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
b0f274d1
SBS
139# Get sorted list of files
140mapfile -t sorted_files < <(printf '%s\n' "${!file_durations[@]}" | sort)
141
b0f274d1
SBS
142# Find the file and offset corresponding to the random starting point
143accumulated_duration=0
00fcff36
SBS
144accumulated_duration_int=0
145
146for idx in "${!sorted_files[@]}"; do
147 file="${sorted_files[$idx]}"
b0f274d1 148 duration="${file_durations["$file"]}"
00fcff36
SBS
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)
b0f274d1
SBS
164 yell "Starting playback from $offset seconds into file: $file"
165
00fcff36
SBS
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
b0f274d1
SBS
180
181 # Start playback using mpv
00fcff36 182 mpv "$edl_file"
b0f274d1 183
00fcff36
SBS
184 # Securely delete the EDL file
185 if [[ -n "$edl_file" && -f "$edl_file" ]]; then
186 rm "$edl_file"
187 exit 0
b0f274d1 188 else
00fcff36
SBS
189 die "FATAL: edl_file is either unset or not a valid file, skipping deletion."
190 fi
b0f274d1 191 fi
00fcff36
SBS
192 accumulated_duration="$new_accumulated_duration"
193 accumulated_duration_int="$new_accumulated_duration_int"
b0f274d1
SBS
194done
195
00fcff36 196die "FATAL: Could not find file corresponding to random point."