-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclubs.bash
executable file
·433 lines (397 loc) · 16.4 KB
/
clubs.bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
#!/usr/bin/env bash
# This script helps organize music files (either FLAC or OGG).
# Return codes:
# 0 Execution terminated faithfully
# 1 User-related issue (wrong parameters...)
# 2 External issue (directory permission...)
# 3 Cannot parse source file data
# 4 Build-related problem
# Safeguards
# -u not specified because of associative array use
# -e not specified to allow proper error management
set -o pipefail
IFS=$'\n\t'
SCRIPT_REAL_PATH=$(dirname "${0}")
readonly SCRIPT_REAL_PATH
source "${SCRIPT_REAL_PATH}/utils.bash"
# Globals
## Once set, they should be read-only
## Parameters
declare source
declare target
declare format
declare placeholder
# CONSTANTS
## taking windows limitations in account, to guarantee maximum portability
declare -r REPLACER_MARKER="?"
declare -r ADDITION_MARKER="+"
declare -r TOKEN_COMPOSITION_OPENER="{"
declare -r TOKEN_COMPOSITION_CLOSER="}"
declare -r TOKEN_COMPOSITION_CLOSERS="${TOKEN_COMPOSITION_CLOSER}${REPLACER_MARKER}${ADDITION_MARKER}"
declare -r DEFAULT_PLACEHOLDER="_"
declare -r DEFAULT_FORMAT="{album_artist}/{orig_year?year+ – }{album}/{disc+-}{track?track_number} – {title}.{extension}"
declare -r -A AVAILABLE_TAGS=( [ALBUM]=ALBUM [ALBUMARTIST]=ALBUMARTIST [ARTIST]=ARTIST \
[BARCODE]=BARCODE [BPM]=BPM [BY]=BY [CATALOGID]=CATALOGID \
[CATALOGNUMBER]=CATALOGNUMBER [COMPOSER]=COMPOSER [CONDUCTOR]=CONDUCTOR \
[COPYRIGHT]=COPYRIGHT [COUNTRY]=COUNTRY [CREDITS]=CREDITS \
[DATE]=DATE [DESCRIPTION]=DESCRIPTION [DISCNUMBER]=DISCNUMBER \
[DISCTOTAL]=DISCTOTAL [ENCODEDBY]=ENCODEDBY [GAIN]=GAIN \
[GENRE]=GENRE [GROUPING]=GROUPING [ID]=ID [ISRC]=ISRC \
[LABEL]=LABEL [LANGUAGE]=LANGUAGE [LENGTH]=LENGTH [LOCATION]=LOCATION \
[LYRICS]=LYRICS [MCDI]=MCDI [MEDIA]=MEDIA [MEDIATYPE]=MEDIATYPE \
[NORM]=NORM [ORGANIZATION]=ORGANIZATION [ORIGYEAR]=ORIGYEAR \
[PEAK]=PEAK [PERFORMER]=PERFORMER [PGAP]=PGAP [PMEDIA]=PMEDIA \
[PROVIDER]=PROVIDER [PUBLISHER]=PUBLISHER [RELEASECOUNTRY]=RELEASECOUNTRY \
[SMPB]=SMPB [STYLE]=STYLE [TBPM]=TBPM [TITLE]=TITLE [TLEN]=TLEN \
[TMED]=TMED [TOOL]=TOOL [TOTALDISCS]=TOTALDISCS [TOTALTRACKS]=TOTALTRACKS \
[TRACKNUMBER]=TRACKNUMBER [TRACKTOTAL]=TRACKTOTAL \
[TSRC]=TSRC [TYPE]=TYPE [UPC]=UPC [UPLOADER]=UPLOADER [URL]=URL \
[WEBSITE]=WEBSITE [WMCOLLECTIONID]=WMCOLLECTIONID [WORK]=WORK \
[WWW]=WWW [WWWAUDIOFILE]=WWWAUDIOFILE [WWWAUDIOSOURCE]=WWWAUDIOSOURCE \
# aliases
[DISC]=DISCNUMBER [TRACK]=TRACK [YEAR]=DATE \
# special tags
[EXTENSION]=extension )
HELP_TEXT="Usage ${0} --input <DIRECTORY> --output <DIRECTORY> --format <PATTERN> [OPTIONS]...
Parameters:
-i --input <DIRECTORY> Input folder, containing the music files
-o --output <DIRECTORY> Output folder, into which music files will be transcoded
-f --format <PATTERN> Pattern used to format file names.
Default value is ${DEFAULT_FORMAT}.
-p --placeholder <CHAR> Placeholder character used to replace forbidden characters.
Default value is ${DEFAULT_PLACEHOLDER}."
readonly HELP_TEXT
################################################################################
# Format a raw key by removing underscores and uppercase the string.
# Also, checks if the key is an accepted tag.
# Arguments:
# raw! *string* to format.
# Returns:
# echoes formatted key
# 0 valid key (not amongst AVAILABLE_TAGS)
# 1 invalid key
################################################################################
function get_tag_from_formatter () {
local _raw="${1}"
local _formatted="${_raw/_/}"
local _formatted="${_formatted^^}"
if [[ -n ${AVAILABLE_TAGS[$_formatted]+x} ]] ; then
echo "${AVAILABLE_TAGS[$_formatted]}"
return 0
fi
return 1
}
################################################################################
# Compute a filename for a globally set music file.
# Arguments:
# +format! *pattern* to compute filename
# +placeholder! *char* to replace forbidden ones with
# Returns:
# echoes formatted filename
# 0 situation nominal
# 1 invalid format
# 2 invalid tag
################################################################################
function build_file_name () {
# vars
local _raw
local _tokens=()
# operation flags
local _should_zap_till_closer=0
local _should_pile_on_till_closer=0
while read -r -n 1 _latest ; do
if [[ -z ${_latest+x} ]] ; then
continue
fi
# if an addendum marker was encountered before,
# ignore new tokens until a strict closure marker
if [[ ${_should_zap_till_closer} -eq 1 ]] ; then
if [[ ${_latest} == "${TOKEN_COMPOSITION_CLOSER}" ]] ; then
_should_zap_till_closer=0
fi
continue
fi
# if a replacer marker was encountered before,
# pile all the new tokens until any closure marker
if [[ ${_should_pile_on_till_closer} -eq 1 ]] ; then
if [[ ${_latest} =~ [${TOKEN_COMPOSITION_CLOSERS}] ]] ; then
_should_pile_on_till_closer=0
# stop handling closure, zap until it ends
if [[ ! ${_latest} == "${TOKEN_COMPOSITION_CLOSER}" ]] ; then
_should_zap_till_closer=1
fi
else
_tokens+=( "${_latest}" )
fi
continue
fi
# currently not composing a token
if [[ -z ${_aggregated+x} ]] ; then
if [[ ${_latest} == "${TOKEN_COMPOSITION_CLOSERS}" ]] ; then
return 1
elif [[ ${_latest} == "${TOKEN_COMPOSITION_OPENER}" ]] ; then
_aggregated=""
else
_tokens+=( "${_latest}" )
fi
# currently composing a token
else
if [[ ${_latest} =~ [${TOKEN_COMPOSITION_CLOSERS}] ]] ; then
if [[ -z ${_aggregated+x} \
|| ${_latest} == "${TOKEN_COMPOSITION_OPENER}" ]] ; then
return 1
fi
_token=$(get_tag_from_formatter "${_aggregated}")
case "${?}" in
0 ) ;;
1 ) err "Wrong token ${_aggregated}"; return 2;;
esac
unset _aggregated
_raw="$(extract_music_file_data "${_token}")"
case "${?}" in
0 ) # if value found, stop handling closure
debug "Extracted '${_raw}' with ${_token}"
_tokens+=( "$(sanitize_path "${_raw}")" )
case "${_latest}" in
"${ADDITION_MARKER}" )
debug "Addendum detected, stockpiling next characters"
_should_pile_on_till_closer=1
;;
"${REPLACER_MARKER}" )
debug "Replacer detected but a value was present"
_should_zap_till_closer=1
;;
"${TOKEN_COMPOSITION_CLOSER}" )
;;
esac
;;
1 ) # if tag comes up empty, check if a replacer is available
# or insert the placeholder character
debug "Key ${_token} not found"
case "${_latest}" in
"${ADDITION_MARKER}" )
debug "Addendum detected, but no base value was present"
_should_zap_till_closer=1
;;
"${REPLACER_MARKER}" )
debug "Replacer detected, analyzing next token"
_aggregated=""
;;
"${TOKEN_COMPOSITION_CLOSER}" )
_tokens+=( "${placeholder}" )
;;
esac
;;
esac
unset _raw
else
_aggregated="${_aggregated}${_latest}"
fi
fi
done <<< "${format}"
printf "%s" "${_tokens[@]}"
return 0
}
################################################################################
# Parse arguments from the command line and set global parameters.
# Arguments:
# arguments!* *array* of arguments from the command line
# Sets globals:
# source! *path* to source directory
# target! *path* to target directory
# format? *string* modeling the expected file path & name
# should_move_files? *flag* indicating to move / delete files
# log_level? *integer* representing the logging level
# Returns:
# 0 arguments parsed faithfully
# 1 Unknown option
# 2 Bad option
# 10 Help displayed
################################################################################
function parse_arguments () {
debug "Parsing arguments"
while : ; do
case "${1}" in
-i | --input)
# not readonly because of subsequent formatting
source="${2}"
shift 2
;;
-o | --output)
target="${2}"
shift 2
;;
-f | --format)
format="${2}"
readonly format
shift 2
;;
-p | --placeholder)
placeholder="${2}"
readonly placeholder
shift 2
;;
--) # End of all options
shift
break
;;
-*) # Unknown
parse_common_arguments "${@}"
case "${?}" in
0 ) debug "Parsed common argument ${1}"; shift "${SHIFT}" ;;
1 ) return 1 ;;
2 ) break ;;
10) return 10 ;;
esac
;;
*) # No more options
break
;;
esac
done
debug "Parsed arguments"
}
################################################################################
# Performs operations on global arguments:
# - checks if correctly set for mandatory arguments
# - sets defaults for optional arguments
# Sets globals:
# format? *pattern* with which file name will be formatted
# placeholder? *char* to replace forbidden characters with
# Returns
# 0 situation nominal
# 1 user-related issue (wrong parameter, refusal...)
# 2 missing parameter
# 3 external issue (permissions, path exists as file...)
################################################################################
function check_arguments_validity () {
check_input_argument "${source}"
case "${?}" in
0) ;;
*) return ${?}
esac
readonly source
check_output_argument "${target}"
case "${?}" in
0) ;;
*) return ${?}
esac
readonly target
# Optional parameters
if [[ -z ${format+x} ]] ; then
format="${DEFAULT_FORMAT}"
readonly format
fi
if [[ -z ${placeholder+x} ]] ; then
placeholder="${DEFAULT_PLACEHOLDER}"
readonly placeholder
fi
# Debug vitals
debug "Input: ${source}"
debug "Output: ${target}"
debug "Format: ${format}"
debug "Placeholder: ${placeholder}"
}
################################################################################
# Does the heavy lifting. Will find and organize music files using the provided
# format.
# Uses globals:
# source
# target
# format
# placeholder
# should_move_files
# log_level
# Returns:
# 0 completed transcoding
# 1 no files in source
# 2 destination folder is a file
# 3 problem parsing file data
################################################################################
function main () {
find_music_files "${source}"
declare -r -i music_files_count=${#MUSIC_FILES[@]}
debug "${music_files_count} files found"
if [[ ${#music_files_count} -eq 0 ]] ; then
return 1
fi
music_files_handled_count=0
# Heavy lifting
for input in "${MUSIC_FILES[@]}" ; do
debug "Probing ${input}"
# Analyze existing music file
ffprobe_music_file "${input}" 1 # into RAW_MUSIC_FILE_STREAM
music_data_to_dictionary "${RAW_MUSIC_FILE_STREAM[@]}" # into DICTIONARY
case "${?}" in
0) ;;
1) return 3 ;;
esac
# var DICTIONARY is referenced as input_stream_data
declare -n "input_stream_data=DICTIONARY"
export output_extension="${input##*.}"
# Compute file name and destination
destination=$(build_file_name)
case "${?}" in
0 ) ;;
1 ) err "Failed to build file name"; return 4 ;;
esac
destination="${target}/${destination}"
log "Filename built: $destination"
# Check destination is valid
check_path_exists_and_is_directory "$(dirname "${destination}")"
case "${?}" in
0 ) ;;
3 ) err "Path exists but is a file rather than directory"; return 2 ;;
4 ) err "Destination path contains forbidden characters"; return 1 ;;
esac
# Move or copy file to destination
if [[ ${should_move_files:?} -eq 1 ]] ; then
if [[ ${is_dry_run:?} -eq 0 ]] ; then
mv "${input}" "${destination}"
fi
debug "Moved ${input} to ${destination}"
else
if [[ ${is_dry_run} -eq 0 ]] ; then
cp "${input}" "${destination}"
fi
debug "Copied ${input} to ${destination}"
fi
log "Done !"
# Stats
(( music_files_handled_count+=1 ))
ratio=$(( (100 * music_files_handled_count) / music_files_count ))
log "Handled ${ratio}% of all files (${music_files_handled_count}/${music_files_count})"
unset ratio
unset output_extension
unset destination
unset raw_stream_data
done
log "All done, congratulations!"
}
# Core
parse_arguments "${@}"
case "${?}" in
0 ) ;;
1 ) err "Unrecognized argument"; exit 1;;
2 ) err "Bad argument"; exit 1;;
10) debug "Help was displayed"; exit 0;;
* ) err "Unrecognized error"; exit 255 ;;
esac
check_common_arguments
check_arguments_validity
case "${?}" in
0 ) ;;
1 ) err "User input issue (wrong parameter, refusal...)"; exit 1;;
2 ) err "Missing parameter"; display_help; exit 1;;
3 ) err "External dysfunction (wrong permissions, path exists but is a file...)"; exit 2;;
* ) err "Unrecognized error"; exit 255;;
esac
main
case "${?}" in
0 ) ;;
1 ) err "No files in source"; exit 0;;
2 ) err "Destination folder is a file"; exit 2;;
3 ) err "Cannot parse source file data"; exit 3;;
4 ) err "Builder-related error"; exit 4;;
* ) err "Unrecognized error"; exit 255 ;;
esac