Commit | Line | Data |
---|---|---|
75e92939 | 1 | #!/usr/bin/env bash |
75baf701 | 2 | # Desc: Wrapper for mpv that accepts directory or file paths via posargs or stdin lines |
75e92939 | 3 | # Usage: bkmpv2 [DIR] |
75baf701 | 4 | # Version: 0.1.0 |
75e92939 SBS |
5 | # Depends: GNU Parallel, GNU Bash v5.1.16, mpv v0.34.1, bc v1.07.1 |
6 | # Ref/Attrib: [1] Tange, Ole. GNU Parallel with Bash Array. 2019-03-24. https://unix.stackexchange.com/a/508365/411854 | |
7 | # Example: find $HOME/Music -type d | bkmpv2 | |
8 | # Example: bkmpv2 $HOME/Music/ | |
75baf701 | 9 | # Example: find $HOME -type f -name "*.mp3" | bkmpv2 |
75e92939 SBS |
10 | # Note: Does not follow symlinks |
11 | ||
12 | # Find settings | |
13 | firegex=".+\(aac\|aif\|aiff\|flac\|m4a\|mp3\|mp4\|ogg\|opus\|wav\)$"; # update according to `find . -type f | grep -Eo "\.([[:alnum:]])+$" | sort -u` | |
14 | fsize="10k"; # default: minimum "10k" | |
15 | fdepth_posarg="10"; # find depth for positional arguments | |
16 | fdepth_stdin="1"; # find depth for stdin | |
17 | fc_falloff="1000"; # characteristic file count falloff in the output | |
18 | fc_redbase="10"; # logarithm base to reduce output file count | |
19 | ||
20 | export firegex fsize ; # export for parallel | |
21 | ||
22 | #===Declare local functions=== | |
23 | yell() { echo "$0: $*" >&2; } # print script path and all args to stderr | |
24 | die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status | |
25 | must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails | |
26 | checkapp() { | |
27 | # Desc: If arg is a command, save result in assoc array 'appRollCall' | |
28 | # Usage: checkapp arg1 arg2 arg3 ... | |
29 | # Version: 0.1.1 | |
30 | # Input: global assoc. array 'appRollCall' | |
31 | # Output: adds/updates key(value) to global assoc array 'appRollCall' | |
32 | # Depends: bash 5.0.3 | |
75baf701 | 33 | local returnState |
75e92939 SBS |
34 | |
35 | #===Process Args=== | |
36 | for arg in "$@"; do | |
75baf701 SBS |
37 | if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command |
38 | appRollCall[$arg]="true"; | |
39 | if ! [ "$returnState" = "false" ]; then returnState="true"; fi; | |
40 | else | |
41 | appRollCall[$arg]="false"; returnState="false"; | |
42 | fi; | |
75e92939 SBS |
43 | done; |
44 | ||
45 | #===Determine function return code=== | |
46 | if [ "$returnState" = "true" ]; then | |
75baf701 | 47 | return 0; |
75e92939 | 48 | else |
75baf701 | 49 | return 1; |
75e92939 SBS |
50 | fi; |
51 | } # Check that app exists | |
52 | checkfile() { | |
53 | # Desc: If arg is a file path, save result in assoc array 'fileRollCall' | |
54 | # Usage: checkfile arg1 arg2 arg3 ... | |
55 | # Version: 0.1.1 | |
56 | # Input: global assoc. array 'fileRollCall' | |
57 | # Output: adds/updates key(value) to global assoc array 'fileRollCall'; | |
58 | # Output: returns 0 if app found, 1 otherwise | |
59 | # Depends: bash 5.0.3 | |
60 | local returnState | |
61 | ||
62 | #===Process Args=== | |
63 | for arg in "$@"; do | |
75baf701 SBS |
64 | if [ -f "$arg" ]; then |
65 | fileRollCall["$arg"]="true"; | |
66 | if ! [ "$returnState" = "false" ]; then returnState="true"; fi; | |
67 | else | |
68 | fileRollCall["$arg"]="false"; returnState="false"; | |
69 | fi; | |
75e92939 | 70 | done; |
75baf701 | 71 | |
75e92939 SBS |
72 | #===Determine function return code=== |
73 | if [ "$returnState" = "true" ]; then | |
75baf701 | 74 | return 0; |
75e92939 | 75 | else |
75baf701 | 76 | return 1; |
75e92939 SBS |
77 | fi; |
78 | } # Check that file exists | |
79 | checkdir() { | |
80 | # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' | |
81 | # Usage: checkdir arg1 arg2 arg3 ... | |
82 | # Version 0.1.1 | |
83 | # Input: global assoc. array 'dirRollCall' | |
84 | # Output: adds/updates key(value) to global assoc array 'dirRollCall'; | |
85 | # Output: returns 0 if app found, 1 otherwise | |
86 | # Depends: Bash 5.0.3 | |
87 | local returnState | |
88 | ||
89 | #===Process Args=== | |
90 | for arg in "$@"; do | |
75baf701 SBS |
91 | if [ -d "$arg" ]; then |
92 | dirRollCall["$arg"]="true"; | |
93 | if ! [ "$returnState" = "false" ]; then returnState="true"; fi | |
94 | else | |
95 | dirRollCall["$arg"]="false"; returnState="false"; | |
96 | fi | |
75e92939 | 97 | done |
75baf701 | 98 | |
75e92939 SBS |
99 | #===Determine function return code=== |
100 | if [ "$returnState" = "true" ]; then | |
75baf701 | 101 | return 0; |
75e92939 | 102 | else |
75baf701 | 103 | return 1; |
75e92939 SBS |
104 | fi |
105 | } # Check that dir exists | |
106 | displayMissing() { | |
107 | # Desc: Displays missing apps, files, and dirs | |
108 | # Usage: displayMissing | |
109 | # Version 0.1.1 | |
110 | # Input: associative arrays: appRollCall, fileRollCall, dirRollCall | |
111 | # Output: stderr: messages indicating missing apps, file, or dirs | |
112 | # Depends: bash 5, checkAppFileDir() | |
113 | local missingApps value appMissing missingFiles fileMissing | |
114 | local missingDirs dirMissing | |
75baf701 | 115 | |
75e92939 SBS |
116 | #==BEGIN Display errors== |
117 | #===BEGIN Display Missing Apps=== | |
118 | missingApps="Missing apps :"; | |
119 | #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done | |
120 | for key in "${!appRollCall[@]}"; do | |
75baf701 SBS |
121 | value="${appRollCall[$key]}"; |
122 | if [ "$value" = "false" ]; then | |
123 | #echo "DEBUG:Missing apps: $key => $value"; | |
124 | missingApps="$missingApps""$key "; | |
125 | appMissing="true"; | |
126 | fi; | |
75e92939 SBS |
127 | done; |
128 | if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing. | |
75baf701 | 129 | echo "$missingApps" 1>&2; |
75e92939 SBS |
130 | fi; |
131 | unset value; | |
132 | #===END Display Missing Apps=== | |
133 | ||
134 | #===BEGIN Display Missing Files=== | |
135 | missingFiles="Missing files:"; | |
136 | #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done | |
137 | for key in "${!fileRollCall[@]}"; do | |
75baf701 SBS |
138 | value="${fileRollCall[$key]}"; |
139 | if [ "$value" = "false" ]; then | |
140 | #echo "DEBUG:Missing files: $key => $value"; | |
141 | missingFiles="$missingFiles""$key "; | |
142 | fileMissing="true"; | |
143 | fi; | |
75e92939 SBS |
144 | done; |
145 | if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing. | |
75baf701 | 146 | echo "$missingFiles" 1>&2; |
75e92939 SBS |
147 | fi; |
148 | unset value; | |
149 | #===END Display Missing Files=== | |
150 | ||
151 | #===BEGIN Display Missing Directories=== | |
152 | missingDirs="Missing dirs:"; | |
153 | #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done | |
154 | for key in "${!dirRollCall[@]}"; do | |
75baf701 SBS |
155 | value="${dirRollCall[$key]}"; |
156 | if [ "$value" = "false" ]; then | |
157 | #echo "DEBUG:Missing dirs: $key => $value"; | |
158 | missingDirs="$missingDirs""$key "; | |
159 | dirMissing="true"; | |
160 | fi; | |
75e92939 SBS |
161 | done; |
162 | if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing. | |
75baf701 | 163 | echo "$missingDirs" 1>&2; |
75e92939 SBS |
164 | fi; |
165 | unset value; | |
166 | #===END Display Missing Directories=== | |
167 | ||
168 | #==END Display errors== | |
169 | } # Display missing apps, files, dirs | |
170 | check_depends() { | |
171 | if ! checkapp mpv parallel bkshuf bc b2sum; then | |
172 | displayMissing; | |
173 | die "FATAL:Missing apps."; | |
174 | fi; | |
175 | return 1; | |
176 | }; # check dependencies | |
177 | checkInt() { | |
178 | # Desc: Checks if arg is integer | |
179 | # Usage: checkInt arg | |
180 | # Input: arg: integer | |
181 | # Output: - return code 0 (if arg is integer) | |
182 | # - return code 1 (if arg is not integer) | |
183 | # Example: if ! checkInt $arg; then echo "not int"; fi; | |
184 | # Version: 0.0.1 | |
185 | local returnState | |
186 | ||
187 | #===Process Arg=== | |
188 | if [[ $# -ne 1 ]]; then | |
75baf701 | 189 | die "ERROR:Invalid number of arguments:$#"; |
75e92939 | 190 | fi; |
75baf701 | 191 | |
75e92939 SBS |
192 | RETEST1='^[0-9]+$'; # Regular Expression to test |
193 | if [[ ! $1 =~ $RETEST1 ]] ; then | |
75baf701 | 194 | returnState="false"; |
75e92939 | 195 | else |
75baf701 | 196 | returnState="true"; |
75e92939 SBS |
197 | fi; |
198 | ||
199 | #===Determine function return code=== | |
200 | if [ "$returnState" = "true" ]; then | |
75baf701 | 201 | return 0; |
75e92939 | 202 | else |
75baf701 | 203 | return 1; |
75e92939 SBS |
204 | fi; |
205 | } # Checks if arg is integer | |
206 | read_stdin() { | |
207 | # Desc: Consumes stdin; outputs as stdout lines | |
208 | # Input: stdin (consumes) | |
209 | # Output: stdout (newline delimited) | |
210 | # Example: printf "foo\nbar\n" | read_stdin | |
211 | # Depends: GNU bash (version 5.1.16) | |
212 | # Version: 0.0.1 | |
213 | local input_stdin output; | |
214 | ||
215 | # Store stdin | |
216 | if [[ -p /dev/stdin ]]; then | |
217 | input_stdin="$(cat -)"; | |
75baf701 SBS |
218 | fi; |
219 | ||
75e92939 SBS |
220 | # Store as output array elements |
221 | ## Read in stdin | |
222 | if [[ -n $input_stdin ]]; then | |
223 | while read -r line; do | |
224 | output+=("$line"); | |
225 | done < <(printf "%s\n" "$input_stdin"); | |
226 | fi; | |
227 | ||
228 | # Print to stdout | |
229 | printf "%s\n" "${output[@]}"; | |
230 | }; # read stdin to stdout lines | |
231 | read_psarg() { | |
232 | # Desc: Reads arguments; outputs as stdout lines | |
233 | # Input: args | |
234 | # Output: stdout (newline delimited) | |
235 | # Example: read_psarg "$@" | |
236 | # Depends: GNU bash (version 5.1.16) | |
237 | # Version: 0.0.1 | |
238 | local input_psarg output; | |
75baf701 | 239 | |
75e92939 SBS |
240 | # Store arguments |
241 | if [[ $# -gt 0 ]]; then | |
242 | input_psarg="$*"; | |
243 | fi; | |
75baf701 | 244 | |
75e92939 SBS |
245 | # Store as output array elements |
246 | ## Read in positional arguments | |
247 | if [[ -n $input_psarg ]]; then | |
248 | for arg in "$@"; do | |
249 | output+=("$arg"); | |
250 | done; | |
251 | fi; | |
252 | ||
253 | # Print to stdout | |
254 | printf "%s\n" "${output[@]}"; | |
255 | }; # read positional argument to stdout lines | |
256 | find_flist() { | |
257 | # Desc: print file list to stdout via `find` using script parameters | |
258 | # Input: arg1: path to dir | |
259 | # var: find_depth | |
260 | # var: pattern_find_iregex | |
261 | # var: find_size | |
262 | if [[ ! -d "$1" ]]; then return 1; fi; | |
263 | must find "$1" -maxdepth "$fdepth" -type f -iregex "$firegex" -size +"$fsize"; | |
264 | }; # print file list to stdout from dir with script parameters | |
265 | main() { | |
266 | # Input: var: firegex find iregex file name pattern | |
267 | # var: fsize find minimum file siz | |
268 | # var: fc_falloff characteristic file count falloff in the output | |
269 | # var: fc_redbase logarithm base to reduce output file count | |
270 | ||
271 | local re_dotfile; | |
75baf701 | 272 | declare -a files_stdin dirs_stdin dirs_psarg; |
75e92939 SBS |
273 | declare -a paths_files; |
274 | declare list_paths_files; | |
275 | declare -a cmd_args; | |
276 | check_depends; | |
277 | ||
278 | yell "STATUS:$SECONDS:Started."; | |
279 | #Populate dirs_stdin and dirs_psarg arrays | |
280 | ## Read stdin as lines | |
281 | re_dotfile="^\."; # first char is a dot | |
282 | while read -r line; do | |
283 | line="$(readlink -e "$line")"; | |
75baf701 SBS |
284 | line_bn="$(basename "$line")"; |
285 | # Check if dir and not dotfile | |
286 | if [[ -d "$line" ]] && [[ ! "$line_bn" =~ $re_dotfile ]]; then | |
287 | dirs_stdin+=("$line"); | |
75e92939 SBS |
288 | continue; |
289 | fi; | |
75baf701 SBS |
290 | |
291 | # Check if file | |
292 | if [[ -f "$line" ]]; then | |
293 | files_stdin+=("$line"); | |
75e92939 SBS |
294 | continue; |
295 | fi; | |
75baf701 SBS |
296 | |
297 | # Throw warning | |
298 | yell "WARNING:Not a valid dir or file:$line"; | |
299 | done < <( read_stdin; read_psarg "$@"; ); | |
300 | yell "STATUS:$SECONDS:Read stdin and psargs."; | |
301 | ||
75e92939 | 302 | # Catch all arrays empty |
75baf701 SBS |
303 | if [[ "${#dirs_stdin[@]}" -le 0 ]] && \ |
304 | [[ "${#dirs_psarg[@]}" -le 0 ]] && \ | |
305 | [[ "${#files_stdin[@]}" -le 0 ]]; then | |
306 | die "FATAL:No valid directories or files provided."; | |
75e92939 SBS |
307 | fi; |
308 | ||
309 | # Generate file list | |
75baf701 SBS |
310 | ## Add stdin argument input |
311 | if [[ "${#files_stdin[@]}" -gt 0 ]]; then | |
312 | paths_files+=("${files_stdin[@]}"); | |
313 | fi; | |
314 | ||
75e92939 SBS |
315 | ## Call find_filelist() in parallel for positional argument input |
316 | if [[ "${#dirs_psarg[@]}" -gt 0 ]]; then | |
317 | fdepth="$fdepth_posarg"; export fdepth; # for dirs from positional arguments | |
318 | paths_files+=("$( parallel find_flist {} "$fdepth" "$firegex" "$fsize" ::: "${dirs_psarg[@]}" )"); # See [1] | |
319 | fi; | |
320 | ## Call find_filelist() in parallel for stdin input | |
321 | if [[ "${#dirs_stdin[@]}" -gt 0 ]]; then | |
322 | fdepth="$fdepth_stdin"; export fdepth; # 1 for dirs from stdin | |
323 | paths_files+=("$( parallel find_flist {} "$fdepth" "$firegex" "$fsize" ::: "${dirs_stdin[@]}" )"); # See [1] | |
324 | fi; | |
325 | ||
326 | # Convert paths_files array into file list | |
327 | for i in "${!paths_files[@]}"; do | |
328 | list_paths_files="$(printf "%s\n%s" "${paths_files[$i]}" "$list_paths_files")"; | |
329 | done; | |
330 | # Get stats | |
331 | fc="$(wc -l < <(echo -n "$list_paths_files"))"; | |
332 | echo "STATUS:$SECONDS:file count:fc:$fc" | |
333 | yell "STATUS:$SECONDS:Generated file list."; | |
334 | ||
335 | # Sort, remove duplicate paths | |
336 | list_paths_files="$(echo "$list_paths_files" | sort -u | tr -s '\n')"; | |
337 | yell "STATUS:$SECONDS:Sorted and deduplicated list."; | |
338 | ||
339 | # Remove paths with dotfiles | |
340 | list_paths_files="$(echo "$list_paths_files" | grep -viE "/\." )"; | |
341 | yell "STATUS:$SECONDS:Removed dotfiles."; | |
342 | ||
343 | # Write playlist | |
344 | list_paths_files_tmp="/dev/shm/$(date +%Y%m%dT%H%M%S.%N%z)"..mpv_paths.txt; | |
345 | ## Reduce output file count if greater than falloff | |
346 | if [[ $fc -gt $fc_falloff ]]; then | |
347 | bc_exp="$fc_falloff * (1 + l( $fc / $fc_falloff )/l($fc_redbase))"; | |
348 | fc_out="$( echo "$bc_exp" | bc -l )"; | |
349 | else | |
75baf701 | 350 | fc_out="$fc"; |
75e92939 SBS |
351 | fi; |
352 | ## Reduce output file count by fixed fraction (bkshuf optimization) | |
353 | fc_out="$(echo "$fc_out * 0.75" | bc -l)"; | |
354 | ## Select subset via bkshuf | |
355 | ### Specify bkshuf-specific environment variables | |
356 | export BKSHUF_PARAM_LINC=1000000; # for these numbers of lines of input.. | |
357 | export BKSHUF_PARAM_GSIZE=25; # target this group size | |
358 | ### Round file count down | |
359 | fc_out="$(printf "%.0f" "$fc_out")"; # round float down to int | |
360 | ### Get neighbor-preserving shuffled subset (size $fc_out) | |
75baf701 | 361 | yell "STATUS:$SECONDS:Selecting $fc_out of $fc files via bkshuf..."; |
75e92939 SBS |
362 | must \ |
363 | echo -n "$list_paths_files" | \ | |
364 | bkshuf "$fc_out" > "$list_paths_files_tmp"; | |
365 | yell "STATUS:$SECONDS:Wrote playlist."; | |
366 | ||
367 | # Print stats | |
368 | yell "STATUS:$SECONDS:Built file list in $SECONDS seconds."; | |
369 | ||
370 | # Run mpv with filelist | |
371 | ## Form command | |
372 | cmd_args+=("mpv"); | |
373 | cmd_args+=("--audio-display=no"); # disable video for audio files | |
374 | cmd_args+=("--vid=no"); # donʼt show video track | |
375 | cmd_args+=("--image-display-duration=0"); # don't show album art | |
376 | cmd_args+=("--af=scaletempo=stride=15:overlap=1:search=15"); # improve scrubbing | |
377 | cmd_args+=("--playlist=$list_paths_files_tmp"); # read playlist | |
378 | declare -p cmd_args; # debug | |
379 | ## Execute command | |
380 | must "${cmd_args[@]}"; | |
381 | must rm "$list_paths_files_tmp"; | |
382 | }; | |
383 | export -f yell die must read_stdin read_psarg find_flist; | |
384 | ||
385 | main "$@"; | |
386 | ||
387 | # Author: Steven Baltakatei Sandoval | |
388 | # License: GPLv3+ |