]> zdv2.bktei.com Git - BK-2020-03.git/blob - user/bk-copy-rand-music
fix(user/bk-copy-rand-music):Cap opus bitrate to 256k
[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.6.1
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 input music file (bytes)
19 max_file_size="100000000"; # maximum size per input music file (bytes)
20 siz_dest="600000000"; # default destination size limit: 600 MB
21 max_find_depth="10"; # max find depth
22 limit_bitrate="256000"; # maximum bitrate (bps) for output audio files (256000 for opus)
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
305 #yell "DEBUG:Finished get_media_length(). $(declare -p file_in)";
306 } # Get media container length in seconds via stdout
307 checkInt() {
308 # Desc: Checks if arg is integer
309 # Usage: checkInt arg
310 # Input: arg: integer
311 # Output: - return code 0 (if arg is integer)
312 # - return code 1 (if arg is not integer)
313 # Example: if ! checkInt $arg; then echo "not int"; fi;
314 # Version: 0.0.1
315 local returnState
316
317 #===Process Arg===
318 if [[ $# -ne 1 ]]; then
319 die "ERROR:Invalid number of arguments:$#";
320 fi;
321
322 RETEST1='^[0-9]+$'; # Regular Expression to test
323 if [[ ! $1 =~ $RETEST1 ]] ; then
324 returnState="false";
325 else
326 returnState="true";
327 fi;
328
329 #===Determine function return code===
330 if [ "$returnState" = "true" ]; then
331 return 0;
332 else
333 return 1;
334 fi;
335 } # Checks if arg is integer
336 checkIsInArray() {
337 # Desc: Checks if input arg is element in array
338 # Usage: checkIsInArray arg1 arg2
339 # Version: 0.0.1
340 # Input: arg1: test string
341 # arg2: array
342 # Output: exit code 0 if test string is in array; 1 otherwise
343 # Example: checkIsInArray "foo" "${myArray[@]}"
344 # Ref/Attrib: [1] How do I check if variable is an array? https://stackoverflow.com/a/27254437
345 # [2] How to pass an array as function argument? https://askubuntu.com/a/674347
346 local return_state input arg1 string_test
347 declare -a arg2 array_test
348 input=("$@") # See [2]
349 arg1="${input[0]}";
350 arg2=("${input[@]:1}");
351 #yell "DEBUG:input:${input[@]}";
352 #yell "DEBUG:arg1:${arg1[@]}";
353 #yell "DEBUG:arg2:${arg2[@]}";
354
355 string_test="$arg1";
356 array_test=("${arg2[@]}");
357
358 #yell "DEBUG:string_test:$string_test";
359 #yell "DEBUG:$(declare -p array_test)";
360 for element in "${array_test[@]}"; do
361 #yell "DEBUG:element:$element";
362 if [[ "$element" =~ ^"$string_test" ]]; then
363 return_state="true";
364 continue;
365 fi;
366 done;
367
368 # Report exit code
369 if [[ $return_state == "true" ]]; then
370 return 0;
371 else
372 return 1;
373 fi;
374 } # Check if string is element in array
375 get_media_bitrate() {
376 # Use ffprobe to get audio/visual media container bitrate (bits per second integer)
377 # Usage: get_media_bitrate arg1
378 # Input: arg1: str path to file
379 # Output: stdout: int bitrate (bits per second)
380 # Version: 0.0.1
381 # Depends: ffprobe 4.4.2
382 # Ref/Attrib: [1] How to get video duration in seconds? https://superuser.com/a/945604
383 # [2] Determine video bitrate using ffmpeg https://superuser.com/questions/1106343/determine-video-bitrate-using-ffmpeg
384 local file_in
385 file_in="$1";
386 if [[ ! -f $file_in ]]; then
387 die "ERROR:Not a file:$file_in";
388 fi;
389 ffprobe -v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "$file_in";
390
391 #yell "DEBUG:Finished get_media_bitrate with $? on:$1";
392 } # Get media container length in seconds via stdout
393 transcode_copy() {
394 # Desc: Transcode high bitrate file into smaller opus file
395 # Note: Meant for downsizing large lossless FLAC
396 # Input: arg1 str path to input audio file
397 # arg2 str output file path
398 # var limit_bitrate int max output transcode bitrate (bps)
399 # Output: file
400
401 ffmpeg -nostdin -i "$1" -c:a libopus -b:a "${limit_bitrate}" "${2}.opus";
402 }; # transcode to lower bitrate audio file
403
404 main() {
405 # Desc: Main program
406 # Input: arg1: path to source tree
407 # arg2: path to destination tree
408 # arg3: cumulative duration (seconds) of audio files in destination tree
409 # arg4: cumulative size (bytes) of audio files in destination tree (optional)
410 # assoc arrays: appRollCall, fileRollCall, dirRollCall
411 # env.var: BKSHUF_PARAM_LINEC (bkshuf)
412 # BKSHUF_PARAM_GSIZE (bkshuf)
413 # arrays: music_codecs
414 # vars: max_filename_length, min_file_duration, max_file_duration,
415 # min_file_size, max_file_size, siz_dest, max_find_depth
416 # Output: [none]
417 # Depends: yell(), checkdir() 0.1.2, displayMissing() 1.0.0, GNU Coreutils 8.30
418 # BK-2020-03: bkshuf v0.1.0
419 local arg1 arg2 arg3 dur_dest dir_source dir_dest
420 declare -a list_files # array for files to be considered
421 declare -a list_copy # array for files to be copied (string: "$dur,$fsize,$path")
422
423 # Parse args
424 arg1="$1";
425 arg2="$2";
426 arg3="$3";
427 arg4="$4";
428 if ! { [[ $# -eq 3 ]] || [[ $# -eq 4 ]]; }; then
429 showUsage;
430 die "ERROR:Invalid number of args:$#"; fi;
431
432 # Check env vars
433 if ! checkInt "$BKSHUF_PARAM_LINEC"; then
434 die "FATAL:Not an int:BKSHUF_PARAM_LINEC:${BKSHUF_PARAM_LINEC}"; fi;
435 if ! checkInt "$BKSHUF_PARAM_GSIZE"; then
436 die "FATAL:Not an int:BKSHUF_PARAM_LINEC:${BKSHUF_PARAM_GSIZE}"; fi;
437
438 # Check adjustable parameters
439 if ! checkInt "$limit_bitrate"; then
440 die "FATAL:Not an int:limit_bitrate:${limit_bitrate}"; fi;
441
442 ## Check duration
443 if checkInt "$arg3"; then
444 dur_dest="$arg3";
445 else
446 die "FATAL:Duration (seconds) not an int:${arg3}"
447 fi;
448
449 ## Check size
450 if [[ -n "$arg4" ]]; then
451 if checkInt "$arg4"; then
452 siz_dest="$arg4";
453 else
454 die "FATAL:Size (bytes) not an int:${arg4}";
455 fi;
456 fi;
457
458 ## Check directories
459 if checkdir "$arg1" "$arg2"; then
460 dir_source="$arg1";
461 dir_dest="$arg2";
462 else
463 yell "ERROR:Directory error";
464 fi;
465
466 ## Check apps
467 checkapp ffprobe bkshuf;
468
469 if ! displayMissing; then
470 showUsage;
471 die "ERROR:Check missing resources.";
472 fi;
473
474 yell "STATUS:Working...";
475
476 # Populate list_files array
477 while read -r line; do
478 list_files+=("$line");
479 done < <(find -L "$dir_source" -maxdepth "$max_find_depth" -type f | \
480 grep -Ev "$ext_ignore" | \
481 sort);
482
483 # Test and add random elements of list_files to list_copy
484 dur=0; # Initialize duration
485 siz=0; # Initialize size
486 n=0; # Initialize loop counter
487 dur_cand_w=1; # Init duration digit width counter
488 siz_cand_w=1; # Init size digit width counter
489 ## Get element count of list_files array
490 file_count="${#list_files[@]}";
491 while read -r line && \
492 [[ $dur -le $((dur_dest * 95 / 100)) ]] && \
493 [[ $siz -le $((siz_dest * 95 / 100)) ]] && \
494 [[ $n -le $file_count ]]; do
495 ((n++));
496
497 yell "DEBUG:list_copy building loop:$n/$file_count"; # debug
498 printf "DEBUG:%8d,%8d,%8d/%8d,%8d/%8d\n" "$dur_cand" "$siz_cand" "$dur" "$dur_dest" "$siz" "$siz_dest"; # debug
499
500 path_candfile="$line"; # path of candidate file
501
502 ### Check duration
503 dur_cand="$(get_media_length "$path_candfile")"; # length in seconds (float)
504 dur_cand="${dur_cand%%.*}"; # convert float to int
505 if ! checkInt "$dur_cand"; then yell "STATUS:Not an int:dur_cand:${dur_cand}"; continue; fi; # reject
506 if [[ "$((dur + dur_cand))" -gt "$dur_dest" ]]; then yell "STATUS:Will put us over on time."; continue; fi; # reject
507 if [[ "$dur_cand" -lt "$min_file_duration" ]]; then yell "STATUS:Duration too short."; continue; fi; # reject
508 if [[ "$dur_cand" -gt "$max_file_duration" ]]; then yell "STATUS:Duration too long."; continue; fi; # reject
509
510 ### Check raw size
511 siz_cand_raw="$(du -Lb "$path_candfile" | awk '{ print $1 }')"; # size in bytes
512 if ! checkInt "$siz_cand_raw"; then continue; fi; # reject
513 if [[ "$siz_cand_raw" -lt "$min_file_size" ]]; then yell "STATUS:Input file too small."; continue; fi; # reject
514 if [[ "$siz_cand_raw" -gt "$max_file_size" ]]; then yell "STATUS:Input file too large."; continue; fi; # reject
515
516 ### Check effective size
517 bit_cand="$(get_media_bitrate "$path_candfile")"; # bitrate in bps
518 if ! checkInt "$bit_cand"; then die "FATAL:Unable to get bitrate of candidate file. $(declare -p bit_cand path_candfile)"; fi;
519 #### Take into account max output bitrate $limit_bitrate
520 if [[ "$bit_cand" -gt "$limit_bitrate" ]]; then bit_cand="$limit_bitrate"; fi; # assume output bitrate cap
521 siz_cand="$(( (bit_cand * dur_cand) / 8 ))"; # effective size in bytes
522 if ! checkInt "$siz_cand"; then continue; fi; # reject
523 if [[ "$((siz + siz_cand))" -gt "$siz_dest" ]]; then continue; fi; # reject
524 if [[ "$siz_cand" -lt "$min_file_size" ]]; then continue; fi; # reject
525 if [[ "$siz_cand" -gt "$max_file_size" ]]; then continue; fi; # reject
526
527 ### Check if has valid codec
528 if ! check_parsable_audio_ffprobe "$path_candfile"; then continue; fi; # reject
529
530 ### Check if desired codec
531 file_format="$(get_audio_format "$path_candfile")";
532 if ! checkIsInArray "$file_format" "${music_codecs[@]}"; then continue; fi; # reject
533
534 ### Update stats digits widths
535 #### duration
536 dur_cand_wnow="$(printf "%s" "$dur_cand" | wc -m)"; # duration width count
537 if [[ $dur_cand_wnow -gt $dur_cand_w ]]; then
538 dur_cand_w="$dur_cand_wnow"; fi;
539 #### size
540 siz_cand_wnow="$(printf "%s" "$siz_cand" | wc -m)"; # size width count
541 if [[ $siz_cand_wnow -gt $siz_cand_w ]]; then
542 siz_cand_w="$siz_cand_wnow"; fi;
543
544 ### Add/update candfile to array:
545 ### list_copy (array with "duration, size, path")
546 #yell "DEBUG:Adding $path_candfile";
547 printf "DEBUG:%8d,%8d,%s\n" "$dur_cand" "$siz_cand" "$path_candfile" 1>&2;
548 #printf "DEBUG:dur:%s\n" "$dur" 1>&2;
549 #printf "DEBUG:siz:%s\n" "$siz" 1>&2;
550 list_copy+=("$dur_cand,$siz_cand,$path_candfile"); # for copying with order
551
552 ### Update total duration $dur and total size $siz
553 dur="$((dur + dur_cand))";
554 siz="$((siz + siz_cand))";
555 yell "DEBUG:dur:$dur";
556 yell "DEBUG:siz:$siz";
557 done < <(printf "%s\n" "${list_files[@]}" | bkshuf);
558
559 #yell "DEBUG:BKSHUF_PARAM_LINEC:$BKSHUF_PARAM_LINEC";
560 #yell "DEBUG:BKSHUF_PARAM_GSIZE:$BKSHUF_PARAM_GSIZE";
561
562 n=0; # Initialize loop counter
563 num_w="$(printf "%s" "${#list_copy[@]}" | wc -m)"; # init file number format
564 num_fmt="%0""$num_w""d";
565 path_log_output="$dir_dest"/COPY.log;
566 printf "num,fingerprint,duration,size,original_path\n" >> "$path_log_output";
567 # Copy files in list_copy to dir_dest;
568 while read -r line; do
569 #yell "DEBUG:line:$line"; # debug
570 fdur="$(printf "%s" "$line" | cut -d',' -f1)"; # duration in seconds (integer)
571 fsize="$(printf "%s" "$line" | cut -d',' -f2)"; # size in bytes
572 fpath="$(printf "%s" "$line" | cut -d',' -f3-)";
573 fbitrate="$(get_media_bitrate "$fpath")"; # bitrate in bps
574 if ! checkInt "$fbitrate"; then die "FATAL:Invalid bitrate. $(declare -p fpath fbitrate)"; fi;
575 ## Get basename of path
576 file_basename="$(basename "$fpath")";
577 ### Get basename without unprintable non-ASCII characters
578 file_basename_compat="$(printf "%s" "$file_basename" | tr -dc '[:graph:][:space:]' )";
579
580 ## Get 16-character b2sum fingerprint (for different files that share basename)
581 fingerprint="$(b2sum -l32 "$fpath" | awk '{print $1}' )";
582
583 ## Form output filename
584 num="$(printf "$num_fmt" "$n")";
585 file_name="$num"_"$fingerprint".."$file_basename_compat";
586 file_name="${file_name:0:$max_filename_length}"; # Limit filename length (e.g. Windows has max of 255 characters)
587
588 ## Form output path
589 path_output="$dir_dest"/"$file_name";
590
591 ## Copy
592 yell "DEBUG:$(declare -p fdur fsize fbitrate fpath file_basename file_basename_compat fingerprint num file_name path_output)";
593 if [[ "$fbitrate" -lt "$limit_bitrate" ]]; then
594 must cp "$fpath" "$path_output" && yell "NOTICE:Copied ($(printf "%""$dur_cand_w"d "$fdur") seconds): $fpath ";
595 yell "DEBUG:Copied ${file_basename} to ${dir_dest}.";
596 elif [[ "$fbitrate" -ge "$limit_bitrate" ]]; then
597 must transcode_copy "$fpath" "$path_output";
598 yell "DEBUG:Wrote ${limit_bitrate}kbps transcoded ${file_basename} to ${dir_dest}.";
599 fi;
600
601
602 ## Append log
603 fpath_can="$(readlink -f "$fpath")"; # resolve symlinks to canonical path
604 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
605 printf "$log_fmt" "$num" "$fingerprint" "$fdur" "$fsize" "$fpath_can" >> "$path_log_output";
606
607 ((n++));
608 unset file_basename file_basename_compat path_output;
609 done < <(printf "%s\n" "${list_copy[@]}");
610
611 # Report total duration and size
612 yell "NOTICE:Total duration (seconds):$dur";
613 yell "NOTICE:Total size (bytes):$siz";
614
615 } # Main program
616
617 main "$@";
618
619 # Author: Steven Baltakatei Sandoval
620 # License: GPLv3+
621
622 # bkshuf v0.1.0
623 # Author: Steven Baltakatei Sandoval
624 # License: GPLv3+
625 # URL: https://gitlab.com/baltakatei/baltakatei-exdev/-/blob/b9e8b771e985fcdf26ba8b9ccb8e31b62da757d3/unitproc/bkshuf