]> zdv2.bktei.com Git - BK-2020-03.git/blob - user/bk-copy-rand-music
feat(user/bk-copy-rand-music):Compress files surpassing max bitrate
[BK-2020-03.git] / user / bk-copy-rand-music
1 #!/usr/bin/env bash
2 # Desc: Copies random audio files
3 # Usage: bk-copy-rand-music [dir SOURCE] [dir DEST] [int DURATION] ([int BYTES])
4 # Version: 0.5.0
5 # Depends: BK-2020-03: bkshuf v0.1.0
6
7 declare -Ag appRollCall # Associative array for storing app status
8 declare -Ag fileRollCall # Associative array for storing file status
9 declare -Ag dirRollCall # Associative array for storing dir status
10 declare -a music_codecs # Array for storing valid codec names (e.g. "aac" "mp3")
11
12 # Adjustable parameters
13 music_codecs=("vorbis" "aac" "mp3" "flac" "opus" "eac3"); # whitelist of valid codec_names ffprobe might return
14 ext_ignore=".ots\$|.mid\$|.json\$|.gz\$|.jpg\$|.png\$|.asc\$|.pdf\$|.txt\$|.vtt\$|\.SUM|.zip\$|.xz\$|.org\$|.txt\$"; # blacklist of file extensions for 'grep -Evi'
15 max_filename_length="255"; # max output filename length
16 min_file_duration="30"; # minimum duration per music file
17 max_file_duration="3600"; # maximum duration per music file
18 min_file_size="100000"; # minimum size per music file (bytes)
19 max_file_size="100000000"; # maximum size per music file (bytes)
20 siz_dest="600000000"; # default destination size limit: 600 MB
21 max_find_depth="10"; # max find depth
22 limit_bitrate="320000"; # maximum bitrate (bps) for output audio files
23
24 # Load env vars (bkshuf defaults for typical music albums)
25 if [[ ! -v BKSHUF_PARAM_LINEC ]]; then export BKSHUF_PARAM_LINEC=1000000; fi;
26 if [[ ! -v BKSHUF_PARAM_GSIZE ]]; then export BKSHUF_PARAM_GSIZE=10; fi;
27
28 yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
29 die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
30 must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
31 checkapp() {
32 # Desc: If arg is a command, save result in assoc array 'appRollCall'
33 # Usage: checkapp arg1 arg2 arg3 ...
34 # Version: 0.1.1
35 # Input: global assoc. array 'appRollCall'
36 # Output: adds/updates key(value) to global assoc array 'appRollCall'
37 # Depends: bash 5.0.3
38 local returnState
39
40 #===Process Args===
41 for arg in "$@"; do
42 if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
43 appRollCall[$arg]="true";
44 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
45 else
46 appRollCall[$arg]="false"; returnState="false";
47 fi;
48 done;
49
50 #===Determine function return code===
51 if [ "$returnState" = "true" ]; then
52 return 0;
53 else
54 return 1;
55 fi;
56 } # Check that app exists
57 checkfile() {
58 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
59 # Usage: checkfile arg1 arg2 arg3 ...
60 # Version: 0.1.1
61 # Input: global assoc. array 'fileRollCall'
62 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
63 # Output: returns 0 if app found, 1 otherwise
64 # Depends: bash 5.0.3
65 local returnState
66
67 #===Process Args===
68 for arg in "$@"; do
69 if [ -f "$arg" ]; then
70 fileRollCall["$arg"]="true";
71 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
72 else
73 fileRollCall["$arg"]="false"; returnState="false";
74 fi;
75 done;
76
77 #===Determine function return code===
78 if [ "$returnState" = "true" ]; then
79 return 0;
80 else
81 return 1;
82 fi;
83 } # Check that file exists
84 checkdir() {
85 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
86 # Usage: checkdir arg1 arg2 arg3 ...
87 # Version 0.1.2
88 # Input: global assoc. array 'dirRollCall'
89 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
90 # Output: returns 0 if all args are dirs; 1 otherwise
91 # Depends: Bash 5.0.3
92 local returnState
93
94 #===Process Args===
95 for arg in "$@"; do
96 if [ -z "$arg" ]; then
97 dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false";
98 elif [ -d "$arg" ]; then
99 dirRollCall["$arg"]="true";
100 if ! [ "$returnState" = "false" ]; then returnState="true"; fi
101 else
102 dirRollCall["$arg"]="false"; returnState="false";
103 fi
104 done
105
106 #===Determine function return code===
107 if [ "$returnState" = "true" ]; then
108 return 0;
109 else
110 return 1;
111 fi
112 } # Check that dir exists
113 displayMissing() {
114 # Desc: Displays missing apps, files, and dirs
115 # Usage: displayMissing
116 # Version 1.0.0
117 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
118 # Output: stderr: messages indicating missing apps, file, or dirs
119 # Output: returns exit code 0 if nothing missing; 1 otherwise
120 # Depends: bash 5, checkAppFileDir()
121 local missingApps value appMissing missingFiles fileMissing
122 local missingDirs dirMissing
123
124 #==BEGIN Display errors==
125 #===BEGIN Display Missing Apps===
126 missingApps="Missing apps :";
127 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
128 for key in "${!appRollCall[@]}"; do
129 value="${appRollCall[$key]}";
130 if [ "$value" = "false" ]; then
131 #echo "DEBUG:Missing apps: $key => $value";
132 missingApps="$missingApps""$key ";
133 appMissing="true";
134 fi;
135 done;
136 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
137 echo "$missingApps" 1>&2;
138 fi;
139 unset value;
140 #===END Display Missing Apps===
141
142 #===BEGIN Display Missing Files===
143 missingFiles="Missing files:";
144 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
145 for key in "${!fileRollCall[@]}"; do
146 value="${fileRollCall[$key]}";
147 if [ "$value" = "false" ]; then
148 #echo "DEBUG:Missing files: $key => $value";
149 missingFiles="$missingFiles""$key ";
150 fileMissing="true";
151 fi;
152 done;
153 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
154 echo "$missingFiles" 1>&2;
155 fi;
156 unset value;
157 #===END Display Missing Files===
158
159 #===BEGIN Display Missing Directories===
160 missingDirs="Missing dirs:";
161 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
162 for key in "${!dirRollCall[@]}"; do
163 value="${dirRollCall[$key]}";
164 if [ "$value" = "false" ]; then
165 #echo "DEBUG:Missing dirs: $key => $value";
166 missingDirs="$missingDirs""$key ";
167 dirMissing="true";
168 fi;
169 done;
170 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
171 echo "$missingDirs" 1>&2;
172 fi;
173 unset value;
174 #===END Display Missing Directories===
175
176 #==END Display errors==
177 #==BEGIN Determine function return code===
178 if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then
179 return 1;
180 else
181 return 0;
182 fi
183 #==END Determine function return code===
184 } # Display missing apps, files, dirs
185 showUsage() {
186 # Desc: Display script usage information
187 # Usage: showUsage
188 # Version 0.0.1
189 # Input: none
190 # Output: stdout
191 # Depends: GNU-coreutils 8.30 (cat)
192 cat <<'EOF'
193
194 DESCRIPTION:
195 This script may be used to copy a random selection of files containing
196 audio tracks from SOURCE to DEST.
197
198 USAGE:
199 bk-copy-rand-music [dir SOURCE] [dir DEST] [int DURATION] (int BYTES)
200
201 EXAMPLE:
202 bk-copy-rand-music ~/Music /tmp/music-sample 3600
203 bk-copy-rand-music ~/Music /tmp/music-sample 3600 680000000
204
205 DEPENDENCIES:
206 ffprobe
207 GNU Coreutils 8.30
208
209 ENVIRONMENT VARIABLES
210 BKSHUF_PARAM_LINEC (see `bkshuf` in BK-2020-03)
211 BKSHUF_PARAM_GSIZE (see `bkshuf` in BK-2020-03)
212 EOF
213 } # Display information on how to use this script.
214 check_parsable_audio_ffprobe() {
215 # Desc: Checks if ffprobe returns valid audio codec name for file
216 # Usage: check_parsable_audio_ffprobe [path FILE]
217 # Version: 0.0.1
218 # Input: arg1: file path
219 # Output: exit code 0 if returns valid codec name; 1 otherwise
220 # Depends: ffprobe, die()
221 local file_in ffprobe_out
222
223 if [[ $# -ne 1 ]]; then die "ERROR:Invalid number of args:$#"; fi;
224
225 file_in="$1";
226
227 # Check if ffprobe detects an audio stream
228 if ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in" 1>/dev/null 2>&1; then
229 return_state="true";
230 else
231 return_state="false";
232 fi;
233
234 # Fail if ffprobe returns no result
235 ffprobe_out="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")";
236 if [[ -z $ffprobe_out ]]; then
237 return_state="false";
238 fi;
239
240 # Report exit code
241 if [[ $return_state = "true" ]]; then
242 return 0;
243 else
244 return 1;
245 fi;
246 } # Checks if file has valid codec name using ffprobe
247 get_audio_format() {
248 # Desc: Gets audio format of file as string
249 # Usage: get_audio_format arg1
250 # Depends: ffprobe
251 # Version: 0.0.1
252 # Input: arg1: input file path
253 # Output: stdout (if valid audio format)
254 # exit code 0 if audio file; 1 otherwise
255 # Example: get_audio_format myvideo.mp4
256 # Note: Would return "opus" if full ffprobe report had 'Audio: opus, 48000 Hz, stereo, fltp'
257 # Note: Not tested with videos containing multiple video streams
258 # Ref/Attrib: [1] https://stackoverflow.com/questions/5618363/is-there-a-way-to-use-ffmpeg-to-determine-the-encoding-of-a-file-before-transcod
259 # [2] https://stackoverflow.com/questions/44123532/how-to-find-out-the-file-extension-for-extracting-audio-tracks-with-ffmpeg-and-p#comment88464070_50723126
260 local audio_format file_in;
261 local return_state;
262 file_in="$1";
263
264 # Return error exit code if not audio file
265 ## Return error if ffprobe itself exited on error
266 if ! ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in" 1>/dev/null 2>&1; then
267 return_state="false";
268 fi;
269
270 # Get audio format
271 audio_format="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")"; # see [1]
272
273 ## Return error if audio format is incorrectly formatted (e.g. reject if contains spaces)
274 pattern="^[[:alnum:]]+$"; # alphanumeric string with no spaces
275 if [[ $audio_format =~ $pattern ]]; then
276 return_state="true";
277 # Report audio format
278 echo "$audio_format";
279 else
280 return_state="false";
281 fi;
282
283 # Report exit code
284 if [[ $return_state = "true" ]]; then
285 return 0;
286 else
287 return 1;
288 fi;
289 } # Get audio format as stdout
290 get_media_length() {
291 # Use ffprobe to get media container length in seconds (float)
292 # Usage: get_media_length arg1
293 # Input: arg1: path to file
294 # Output: stdout: seconds (float)
295 # Depends: ffprobe 4.1.8
296 # Ref/Attrib: [1] How to get video duration in seconds? https://superuser.com/a/945604
297
298 local file_in
299 file_in="$1";
300 if [[ ! -f $file_in ]]; then
301 die "ERROR:Not a file:$file_in";
302 fi;
303 ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_in";
304 } # Get media container length in seconds via stdout
305 checkInt() {
306 # Desc: Checks if arg is integer
307 # Usage: checkInt arg
308 # Input: arg: integer
309 # Output: - return code 0 (if arg is integer)
310 # - return code 1 (if arg is not integer)
311 # Example: if ! checkInt $arg; then echo "not int"; fi;
312 # Version: 0.0.1
313 local returnState
314
315 #===Process Arg===
316 if [[ $# -ne 1 ]]; then
317 die "ERROR:Invalid number of arguments:$#";
318 fi;
319
320 RETEST1='^[0-9]+$'; # Regular Expression to test
321 if [[ ! $1 =~ $RETEST1 ]] ; then
322 returnState="false";
323 else
324 returnState="true";
325 fi;
326
327 #===Determine function return code===
328 if [ "$returnState" = "true" ]; then
329 return 0;
330 else
331 return 1;
332 fi;
333 } # Checks if arg is integer
334 checkIsInArray() {
335 # Desc: Checks if input arg is element in array
336 # Usage: checkIsInArray arg1 arg2
337 # Version: 0.0.1
338 # Input: arg1: test string
339 # arg2: array
340 # Output: exit code 0 if test string is in array; 1 otherwise
341 # Example: checkIsInArray "foo" "${myArray[@]}"
342 # Ref/Attrib: [1] How do I check if variable is an array? https://stackoverflow.com/a/27254437
343 # [2] How to pass an array as function argument? https://askubuntu.com/a/674347
344 local return_state input arg1 string_test
345 declare -a arg2 array_test
346 input=("$@") # See [2]
347 arg1="${input[0]}";
348 arg2=("${input[@]:1}");
349 #yell "DEBUG:input:${input[@]}";
350 #yell "DEBUG:arg1:${arg1[@]}";
351 #yell "DEBUG:arg2:${arg2[@]}";
352
353 string_test="$arg1";
354 array_test=("${arg2[@]}");
355
356 #yell "DEBUG:string_test:$string_test";
357 #yell "DEBUG:$(declare -p array_test)";
358 for element in "${array_test[@]}"; do
359 #yell "DEBUG:element:$element";
360 if [[ "$element" =~ ^"$string_test" ]]; then
361 return_state="true";
362 continue;
363 fi;
364 done;
365
366 # Report exit code
367 if [[ $return_state == "true" ]]; then
368 return 0;
369 else
370 return 1;
371 fi;
372 } # Check if string is element in array
373 get_media_bitrate() {
374 # Use ffprobe to get audio/visual media container bitrate (bits per second integer)
375 # Usage: get_media_bitrate arg1
376 # Input: arg1: str path to file
377 # Output: stdout: int bitrate (bits per second)
378 # Version: 0.0.1
379 # Depends: ffprobe 4.4.2
380 # Ref/Attrib: [1] How to get video duration in seconds? https://superuser.com/a/945604
381 # [2] Determine video bitrate using ffmpeg https://superuser.com/questions/1106343/determine-video-bitrate-using-ffmpeg
382 local file_in
383 file_in="$1";
384 if [[ ! -f $file_in ]]; then
385 die "ERROR:Not a file:$file_in";
386 fi;
387 ffprobe -v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "$file_in";
388
389 yell "DEBUG:Finished get_media_bitrate with $? on:$1";
390 } # Get media container length in seconds via stdout
391 transcode_copy() {
392 # Desc: Transcode high bitrate file into smaller opus file
393 # Note: Meant for downsizing large lossless FLAC
394 # Input: arg1 str path to input audio file
395 # arg2 str output file path
396 # var limit_bitrate int max output transcode bitrate (bps)
397 # Output: file
398
399 ffmpeg -nostdin -i "$1" -c:a libopus -b:a "${limit_bitrate}" "${2}.opus";
400 }; # transcode to lower bitrate audio file
401
402 main() {
403 # Desc: Main program
404 # Input: arg1: path to source tree
405 # arg2: path to destination tree
406 # arg3: cumulative duration (seconds) of audio files in destination tree
407 # arg4: cumulative size (bytes) of audio files in destination tree (optional)
408 # assoc arrays: appRollCall, fileRollCall, dirRollCall
409 # env.var: BKSHUF_PARAM_LINEC (bkshuf)
410 # BKSHUF_PARAM_GSIZE (bkshuf)
411 # arrays: music_codecs
412 # vars: max_filename_length, min_file_duration, max_file_duration,
413 # min_file_size, max_file_size, siz_dest, max_find_depth
414 # Output: [none]
415 # Depends: yell(), checkdir() 0.1.2, displayMissing() 1.0.0, GNU Coreutils 8.30
416 # BK-2020-03: bkshuf v0.1.0
417 local arg1 arg2 arg3 dur_dest dir_source dir_dest
418 declare -a list_files # array for files to be considered
419 declare -a list_copy # array for files to be copied (string: "$dur,$fsize,$path")
420
421 # Parse args
422 arg1="$1";
423 arg2="$2";
424 arg3="$3";
425 arg4="$4";
426 if ! { [[ $# -eq 3 ]] || [[ $# -eq 4 ]]; }; then
427 showUsage;
428 die "ERROR:Invalid number of args:$#"; fi;
429
430 # Check env vars
431 if ! checkInt "$BKSHUF_PARAM_LINEC"; then
432 die "FATAL:Not an int:BKSHUF_PARAM_LINEC:${BKSHUF_PARAM_LINEC}"; fi;
433 if ! checkInt "$BKSHUF_PARAM_GSIZE"; then
434 die "FATAL:Not an int:BKSHUF_PARAM_LINEC:${BKSHUF_PARAM_GSIZE}"; fi;
435
436 # Check adjustable parameters
437 if ! checkInt "$limit_bitrate"; then
438 die "FATAL:Not an int:limit_bitrate:${limit_bitrate}"; fi;
439
440 ## Check duration
441 if checkInt "$arg3"; then
442 dur_dest="$arg3";
443 else
444 die "FATAL:Duration (seconds) not an int:${arg3}"
445 fi;
446
447 ## Check size
448 if [[ -n "$arg4" ]]; then
449 if checkInt "$arg4"; then
450 siz_dest="$arg4";
451 else
452 die "FATAL:Size (bytes) not an int:${arg4}";
453 fi;
454 fi;
455
456 ## Check directories
457 if checkdir "$arg1" "$arg2"; then
458 dir_source="$arg1";
459 dir_dest="$arg2";
460 else
461 yell "ERROR:Directory error";
462 fi;
463
464 ## Check apps
465 checkapp ffprobe bkshuf;
466
467 if ! displayMissing; then
468 showUsage;
469 die "ERROR:Check missing resources.";
470 fi;
471
472 yell "STATUS:Working...";
473
474 # Populate list_files array
475 while read -r line; do
476 list_files+=("$line");
477 done < <(find -L "$dir_source" -maxdepth "$max_find_depth" -type f | \
478 grep -Ev "$ext_ignore" | \
479 sort);
480
481 # Test and add random elements of list_files to list_copy
482 dur=0; # Initialize duration
483 siz=0; # Initialize size
484 n=0; # Initialize loop counter
485 dur_cand_w=1; # Init duration digit width counter
486 siz_cand_w=1; # Init size digit width counter
487 ## Get element count of list_files array
488 file_count="${#list_files[@]}";
489 while read -r line && \
490 [[ $dur -le $((dur_dest * 95 / 100)) ]] && \
491 [[ $siz -le $((siz_dest * 95 / 100)) ]] && \
492 [[ $n -le $file_count ]]; do
493 ((n++));
494
495 yell "DEBUG:list_copy building loop:$n/$file_count"; # debug
496 printf "DEBUG:%8d,%8d,%8d/%8d,%8d/%8d\n" "$dur_cand" "$siz_cand" "$dur" "$dur_dest" "$siz" "$siz_dest"; # debug
497
498 path_candfile="$line"; # path of candidate file
499
500 ### Check size
501 siz_cand="$(du -Lb "$path_candfile" | awk '{ print $1 }')"; # size in bytes
502 if ! checkInt "$siz_cand"; then continue; fi; # reject
503 if [[ "$((siz + siz_cand))" -gt "$siz_dest" ]]; then continue; fi; # reject
504 if [[ "$siz_cand" -lt "$min_file_size" ]]; then continue; fi; # reject
505 if [[ "$siz_cand" -gt "$max_file_size" ]]; then continue; fi; # reject
506
507 ### Check if has valid codec
508 if ! check_parsable_audio_ffprobe "$path_candfile"; then continue; fi; # reject
509
510 ### Check if desired codec
511 file_format="$(get_audio_format "$path_candfile")";
512 if ! checkIsInArray "$file_format" "${music_codecs[@]}"; then continue; fi; # reject
513
514 ### Check duration
515 dur_cand="$(get_media_length "$path_candfile")"; # length in seconds (float)
516 dur_cand="${dur_cand%%.*}"; # convert float to int
517 if ! checkInt "$dur_cand"; then continue; fi; # reject
518 if [[ "$((dur + dur_cand))" -gt "$dur_dest" ]]; then continue; fi; # reject
519 if [[ "$dur_cand" -lt "$min_file_duration" ]]; then continue; fi; # reject
520 if [[ "$dur_cand" -gt "$max_file_duration" ]]; then continue; fi; # reject
521
522 ### Update stats digits widths
523 #### duration
524 dur_cand_wnow="$(printf "%s" "$dur_cand" | wc -m)"; # duration width count
525 if [[ $dur_cand_wnow -gt $dur_cand_w ]]; then
526 dur_cand_w="$dur_cand_wnow"; fi;
527 #### size
528 siz_cand_wnow="$(printf "%s" "$siz_cand" | wc -m)"; # size width count
529 if [[ $siz_cand_wnow -gt $siz_cand_w ]]; then
530 siz_cand_w="$siz_cand_wnow"; fi;
531
532 ### Add/update candfile to array:
533 ### list_copy (array with "duration, size, path")
534 #yell "DEBUG:Adding $path_candfile";
535 printf "DEBUG:%8d,%8d,%s\n" "$dur_cand" "$siz_cand" "$path_candfile" 1>&2;
536 #printf "DEBUG:dur:%s\n" "$dur" 1>&2;
537 #printf "DEBUG:siz:%s\n" "$siz" 1>&2;
538 list_copy+=("$dur_cand,$siz_cand,$path_candfile"); # for copying with order
539
540 ### Update total duration $dur and total size $siz
541 dur="$((dur + dur_cand))";
542 siz="$((siz + siz_cand))";
543 yell "DEBUG:dur:$dur";
544 yell "DEBUG:siz:$siz";
545 done < <(printf "%s\n" "${list_files[@]}" | bkshuf);
546
547 #yell "DEBUG:BKSHUF_PARAM_LINEC:$BKSHUF_PARAM_LINEC";
548 #yell "DEBUG:BKSHUF_PARAM_GSIZE:$BKSHUF_PARAM_GSIZE";
549
550 n=0; # Initialize loop counter
551 num_w="$(printf "%s" "${#list_copy[@]}" | wc -m)"; # init file number format
552 num_fmt="%0""$num_w""d";
553 path_log_output="$dir_dest"/COPY.log;
554 printf "num,fingerprint,duration,size,original_path\n" >> "$path_log_output";
555 # Copy files in list_copy to dir_dest;
556 while read -r line; do
557 #yell "DEBUG:line:$line"; # debug
558 fdur="$(printf "%s" "$line" | cut -d',' -f1)"; # duration in seconds (integer)
559 fsize="$(printf "%s" "$line" | cut -d',' -f2)"; # size in bytes
560 fpath="$(printf "%s" "$line" | cut -d',' -f3-)";
561 fbitrate="$(get_media_bitrate "$fpath")"; # bitrate in bps
562 if ! checkInt "$fbitrate"; then die "FATAL:Invalid bitrate. $(declare -p fpath fbitrate)"; fi;
563 ## Get basename of path
564 file_basename="$(basename "$fpath")";
565 ### Get basename without unprintable non-ASCII characters
566 file_basename_compat="$(printf "%s" "$file_basename" | tr -dc '[:graph:][:space:]' )";
567
568 ## Get 16-character b2sum fingerprint (for different files that share basename)
569 fingerprint="$(b2sum -l32 "$fpath" | awk '{print $1}' )";
570
571 ## Form output filename
572 num="$(printf "$num_fmt" "$n")";
573 file_name="$num"_"$fingerprint".."$file_basename_compat";
574 file_name="${file_name:0:$max_filename_length}"; # Limit filename length (e.g. Windows has max of 255 characters)
575
576 ## Form output path
577 path_output="$dir_dest"/"$file_name";
578
579 ## Copy
580 yell "DEBUG:$(declare -p fdur fsize fbitrate fpath file_basename file_basename_compat fingerprint num file_name path_output)";
581 if [[ "$fbitrate" -lt "$limit_bitrate" ]]; then
582 must cp "$fpath" "$path_output" && yell "NOTICE:Copied ($(printf "%""$dur_cand_w"d "$fdur") seconds): $fpath ";
583 yell "DEBUG:Copied ${file_basename} to ${dir_dest}.";
584 elif [[ "$fbitrate" -ge "$limit_bitrate" ]]; then
585 must transcode_copy "$fpath" "$path_output";
586 yell "DEBUG:Wrote ${limit_bitrate}kbps transcoded ${file_basename} to ${dir_dest}.";
587 fi;
588
589
590 ## Append log
591 fpath_can="$(readlink -f "$fpath")"; # resolve symlinks to canonical path
592 log_fmt="%s,%s,%""$dur_cand_w""d,%""$siz_cand_w""d,%s\n"; # e.g. "%s,%3d,%5d,%s" if dur_cand_w=3 and siz_cand_w=5
593 printf "$log_fmt" "$num" "$fingerprint" "$fdur" "$fsize" "$fpath_can" >> "$path_log_output";
594
595 ((n++));
596 unset file_basename file_basename_compat path_output;
597 done < <(printf "%s\n" "${list_copy[@]}");
598
599 # Report total duration and size
600 yell "NOTICE:Total duration (seconds):$dur";
601 yell "NOTICE:Total size (bytes):$siz";
602
603 } # Main program
604
605 main "$@";
606
607 # Author: Steven Baltakatei Sandoval
608 # License: GPLv3+
609
610 # bkshuf v0.1.0
611 # Author: Steven Baltakatei Sandoval
612 # License: GPLv3+
613 # URL: https://gitlab.com/baltakatei/baltakatei-exdev/-/blob/b9e8b771e985fcdf26ba8b9ccb8e31b62da757d3/unitproc/bkshuf