]> zdv2.bktei.com Git - BK-2020-03.git/blob - user/rand_media_pl.sh
feat(unitproc/graphu):Tally UTF-8 characters
[BK-2020-03.git] / user / rand_media_pl.sh
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.1.0
11 # Dependencies: Bash 4, ffprobe, mpv, bc, shuf, GNU Parallel
12
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
16
17 # Configurable variables
18 SEARCH_DIR="${1:-.}" # Default to current directory if no argument is provided
19 MAX_DEPTH=8;
20 EXTENSIONS=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm");
21 CACHE_FILE=".playlist_cache";
22
23 declare -gA file_durations;
24 declare total_duration;
25
26 # Function to prompt the user
27 prompt_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
42 find_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
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
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 | parallel readlink -f '{}' | sort -u; );
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 # 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."
87
88 total_duration=0
89 while IFS='|' read -r file duration; do
90 declare -p file duration 1>&2 # Debugging statement
91
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)
96 else
97 yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
98 fi
99 done < "$CACHE_FILE";
100 };
101 # Function to get byte count of file name
102 prepend_path_bc() {
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
111 fin="$1";
112
113 if [[ ! -f "$fin" ]]; then
114 yell "WARNING:File does not exist:${fin}";
115 fi;
116
117 bytecount="$(printf "%s" "$fin" | wc -c; )";
118 re='[0-9]';
119 if [[ ! "$bytecount" =~ $re ]]; then
120 die "FATAL:Not an int:${bytecount}; $(declare -p fin bytecount)";
121 fi;
122 printf "%%%s%%%s" "$bytecount" "$fin";
123 };
124
125 # Main script
126 if [[ -f "$CACHE_FILE" ]]; then
127 if prompt_yes_no "Playlist cache detected. Do you want to use it? (Faster)"; then
128 read_playlist_cache;
129 else
130 generate_playlist_cache;
131 read_playlist_cache;
132 fi;
133 else
134 generate_playlist_cache;
135 read_playlist_cache;
136 fi;
137
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.";
142 fi;
143
144 # Ensure total_duration is not empty
145 if [[ -z "$total_duration" ]]; then
146 die "FATAL: total_duration is empty.";
147 fi;
148
149 # Convert total_duration to an integer
150 total_duration_int=$(printf "%.0f" "$total_duration");
151
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.";
155 fi;
156
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;
160
161 # Get sorted list of files
162 mapfile -t sorted_files < <(printf '%s\n' "${!file_durations[@]}" | sort; );
163
164 # Find the file and offset corresponding to the random starting point
165 accumulated_duration=0;
166 accumulated_duration_int=0;
167
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
172
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")
179 else
180 yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
181 continue
182 fi
183
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";
187
188 # Create an EDL file
189 edl_file=$(mktemp);
190 yell "DEBUG:EDL file at:${edl_file}"; # debug
191 echo "# mpv EDL v0" > "$edl_file";
192
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";
196
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";
204 done;
205
206 # Start playback using mpv
207 mpv "$edl_file" || exit 1;
208
209 # Securely delete the EDL file
210 if [[ -n "$edl_file" && -f "$edl_file" ]]; then
211 rm "$edl_file";
212 exit 0;
213 else
214 die "FATAL: edl_file is either unset or not a valid file, skipping deletion.";
215 fi;
216 fi;
217 accumulated_duration="$new_accumulated_duration";
218 accumulated_duration_int="$new_accumulated_duration_int";
219 done;
220
221 die "FATAL: Could not find file corresponding to random point.";