|
|
@@ -0,0 +1,423 @@ |
|
|
|
#!/bin/bash |
|
|
|
|
|
|
|
set -e |
|
|
|
set -u |
|
|
|
set -o pipefail |
|
|
|
|
|
|
|
# Include files whose name begin with a dot |
|
|
|
shopt -s dotglob |
|
|
|
|
|
|
|
# ${1} path for artist to check |
|
|
|
check_artist() { |
|
|
|
local -r artist_path="${1}" |
|
|
|
local -r artist="$(basename -- "${artist_path}")" |
|
|
|
|
|
|
|
zik_info "artist: ${artist}" |
|
|
|
|
|
|
|
# Check directory permissions |
|
|
|
if [[ "$(stat -L -c "%a" "${artist_path}")" != "755" ]]; then |
|
|
|
zik_warn "uncommon permissions for directory \"${artist_path}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
if [[ "${artist:0:1}" == "." ]]; then |
|
|
|
zik_error "artist name should not start with a dot (${artist})" |
|
|
|
fi |
|
|
|
|
|
|
|
if [[ -z "$(ls -A "${artist_path}")" ]]; then |
|
|
|
zik_error "no album found for artist \"${artist}\"" |
|
|
|
else |
|
|
|
for item in "${artist_path}"/*; do |
|
|
|
if [[ -d "${item}" ]]; then |
|
|
|
check_album "${artist}" "${item}" |
|
|
|
else |
|
|
|
zik_warn "\"${item}\" is not a directory," \ |
|
|
|
"artist path should contain only directories" |
|
|
|
fi |
|
|
|
done |
|
|
|
fi |
|
|
|
} |
|
|
|
|
|
|
|
# ${1} artist name |
|
|
|
# ${2} path for album to check |
|
|
|
check_album() { |
|
|
|
local -r artist="${1}" |
|
|
|
local -r album_path="${2}" |
|
|
|
local -r album="$(basename -- "${album_path}")" |
|
|
|
|
|
|
|
zik_info "album: ${album}" |
|
|
|
|
|
|
|
# Check directory permissions |
|
|
|
if [[ "$(stat -L -c "%a" "${album_path}")" != "755" ]]; then |
|
|
|
zik_warn "uncommon permissions for directory \"${album_path}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
if [[ "${album:0:1}" == "." ]]; then |
|
|
|
zik_error "album name should not start with a dot (${artist} / ${album})" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check for a cover picture |
|
|
|
local -r cover_file="${album_path}/${COVER_PICTURE_FILE}" |
|
|
|
if [[ -f "${cover_file}" ]]; then |
|
|
|
zik_info "found a cover picture for ${artist} / ${album}" |
|
|
|
|
|
|
|
# Check file permissions |
|
|
|
if [[ "$(stat -L -c "%a" "${cover_file}")" != "644" ]]; then |
|
|
|
zik_warn "uncommon permissions for file \"${cover_file}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check cover picture size |
|
|
|
local -r cover_size="$(exiftool -s3 -ImageSize "${cover_file}")" |
|
|
|
if [[ "${cover_size}" != "${COVER_SIZE}" ]]; then |
|
|
|
zik_warn "cover picture size is ${cover_size} for ${artist} / ${album}" |
|
|
|
fi |
|
|
|
else |
|
|
|
zik_warn "no cover picture \"${COVER_PICTURE_FILE}\" found" \ |
|
|
|
"for ${artist} / ${album}" |
|
|
|
fi |
|
|
|
|
|
|
|
local file_name |
|
|
|
if [[ -z "$(ls -A "${album_path}")" ]]; then |
|
|
|
zik_error "no files found for ${artist} / ${album}" |
|
|
|
else |
|
|
|
# Total number of files (no cover) |
|
|
|
# shellcheck disable=SC2012 |
|
|
|
local -r num="$(ls -1q -I "${COVER_PICTURE_FILE}" "${album_path}" | wc -l)" |
|
|
|
|
|
|
|
for item in "${album_path}"/*; do |
|
|
|
if [[ -f "${item}" ]]; then |
|
|
|
file_name="$(basename -- "${item}")" |
|
|
|
|
|
|
|
# Skip cover picture file |
|
|
|
if [[ "${file_name}" = "${COVER_PICTURE_FILE}" ]]; then |
|
|
|
continue |
|
|
|
fi |
|
|
|
|
|
|
|
# Check file type and name (must start with 3 digits and a dash) |
|
|
|
if [[ "${file_name##*.}" =~ ^(flac|mp3|ogg)$ ]]; then |
|
|
|
if [[ "${file_name%.*}" =~ ^[0-9]{3}\ -\ .*$ ]]; then |
|
|
|
check_file "${artist}" "${album}" "${num}" "${item}" |
|
|
|
else |
|
|
|
zik_error "badly formatted file name \"${item}\"" |
|
|
|
fi |
|
|
|
else |
|
|
|
zik_error "unmanaged file \"${item}\"" |
|
|
|
fi |
|
|
|
else |
|
|
|
zik_warn "\"${item}\" is not a file," \ |
|
|
|
"album path should contain only files" |
|
|
|
fi |
|
|
|
done |
|
|
|
fi |
|
|
|
} |
|
|
|
|
|
|
|
# ${1} artist name |
|
|
|
# ${2} album name |
|
|
|
# ${3} number of files in album path (no cover) |
|
|
|
# ${4} path for file to check |
|
|
|
check_file() { |
|
|
|
local -r artist="${1}" |
|
|
|
local -r album="${2}" |
|
|
|
local -r track_total="$(printf "%03d" "${3}")" |
|
|
|
local -r file_path="${4}" |
|
|
|
local -r file_name="$(basename -- "${file_path}")" |
|
|
|
|
|
|
|
# Check file permissions |
|
|
|
if [[ "$(stat -L -c "%a" "${file_path}")" != "644" ]]; then |
|
|
|
zik_warn "uncommon permissions for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Extract track info from file name |
|
|
|
local -r track_num="$(echo "${file_name%.*}" | cut -d " " -f 1)" |
|
|
|
local -r track_name="$(echo "${file_name%.*}" | cut -d " " -f 3-)" |
|
|
|
local -r track_ext="${file_name##*.}" |
|
|
|
|
|
|
|
# Reset exiftool error file size |
|
|
|
truncate -s 0 "${EXIFTOOL_ERROR_FILE}" |
|
|
|
|
|
|
|
# Gather audio file tags |
|
|
|
local -a tags |
|
|
|
# shellcheck disable=SC2016 |
|
|
|
readarray -t tags <<< "$(exiftool -f -p '$FileType' \ |
|
|
|
-p '$FileTypeExtension' \ |
|
|
|
-p '$Title' \ |
|
|
|
-p '$Artist' \ |
|
|
|
-p '$Band' \ |
|
|
|
-p '$Album' \ |
|
|
|
-p '$AlbumArtist' \ |
|
|
|
-p '$Comment' \ |
|
|
|
-p '$Description' \ |
|
|
|
-p '$Genre' \ |
|
|
|
-p '$DiscNumber' \ |
|
|
|
-p '$PartOfSet' \ |
|
|
|
-p '$Track' \ |
|
|
|
-p '$TrackNumber' \ |
|
|
|
-p '$TrackTotal' \ |
|
|
|
-p '$PictureDescription' \ |
|
|
|
-p '$PictureType' \ |
|
|
|
"${file_path}" \ |
|
|
|
2> "${EXIFTOOL_ERROR_FILE}")" |
|
|
|
|
|
|
|
# Manage gathered tags |
|
|
|
local -r exif_file_type="${tags[0]}" |
|
|
|
local -r exif_file_type_extension="${tags[1]}" |
|
|
|
local -r exif_title="${tags[2]}" |
|
|
|
local -r exif_artist="${tags[3]}" |
|
|
|
local -r exif_band="${tags[4]}" |
|
|
|
local -r exif_album="${tags[5]}" |
|
|
|
local -r exif_album_artist="${tags[6]}" |
|
|
|
local -r exif_comment="${tags[7]}" |
|
|
|
local -r exif_description="${tags[8]}" |
|
|
|
local -r exif_genre="${tags[9]}" |
|
|
|
local -r exif_disc_number="${tags[10]}" |
|
|
|
local -r exif_part_of_set="${tags[11]}" |
|
|
|
local -r exif_track="${tags[12]}" |
|
|
|
local -r exif_track_number="${tags[13]}" |
|
|
|
local -r exif_track_total="${tags[14]}" |
|
|
|
local -r exif_picture_description="${tags[15]}" |
|
|
|
local -r exif_picture_type="${tags[16]}" |
|
|
|
|
|
|
|
# Manage exiftool error messages |
|
|
|
if [[ -s "${EXIFTOOL_ERROR_FILE}" ]]; then |
|
|
|
zik_warn "exiftool output error for file \"${file_path}\"" |
|
|
|
while read -r EXIFTOOL_MSG; do |
|
|
|
echo " ${EXIFTOOL_MSG}" |
|
|
|
done < "${EXIFTOOL_ERROR_FILE}" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check type extension |
|
|
|
if [[ "${track_ext}" != "${exif_file_type_extension}" ]]; then |
|
|
|
zik_error "type extension mismatch for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check artist |
|
|
|
if [[ "${artist}" != "${exif_artist}" ]]; then |
|
|
|
zik_warn "artist mismatch for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check album |
|
|
|
local -r album_clean="${album//["/"" ""-""?""'"":""\"""_""*""+"]/}" |
|
|
|
local -r exif_album_clean="${exif_album//["/"" ""-""?""'"":""\"""_""*""+"]/}" |
|
|
|
if [[ "${album_clean}" != "${exif_album_clean}" ]]; then |
|
|
|
zik_warn "album mismatch for file \"${file_path}\"" |
|
|
|
echo " Album from file: \"${album}\"" |
|
|
|
echo " Album from tags: \"${exif_album}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check title |
|
|
|
local -r track_name_clean="${track_name//["/"" ""-""?""'"":""\"""_""*""+"]/}" |
|
|
|
local -r exif_title_clean="${exif_title//["/"" ""-""?""'"":""\"""_""*""+"]/}" |
|
|
|
if [[ "${track_name_clean}" != "${exif_title_clean}" ]]; then |
|
|
|
zik_warn "track title mismatch for file \"${file_path}\"" |
|
|
|
echo " Title from file: \"${track_name}\"" |
|
|
|
echo " Title from tags: \"${exif_title}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check genre |
|
|
|
if [[ "${exif_genre}" != "-" ]]; then |
|
|
|
zik_warn "genre is set to \"${exif_genre}\" for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check picture file name |
|
|
|
if [[ "${exif_picture_description}" != "${COVER_PICTURE_FILE}" ]]; then |
|
|
|
zik_warn "bad embedded picture name for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Check picture type |
|
|
|
if [[ "${exif_picture_type}" != "Front Cover" ]]; then |
|
|
|
zik_warn "embedded picture is not front cover for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
|
|
|
|
# Specific file type checks |
|
|
|
case "${exif_file_type}" in |
|
|
|
FLAC) |
|
|
|
# Check album artist |
|
|
|
if [[ "${exif_album_artist}" != "${exif_artist}" ]]; then |
|
|
|
zik_warn "artist and album artist are different for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check track number |
|
|
|
if [[ "${track_num}" != "${exif_track_number}" ]]; then |
|
|
|
zik_warn "bad track number for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check track total |
|
|
|
if [[ "${track_total}" != "${exif_track_total}" ]]; then |
|
|
|
zik_warn "bad track total for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check for no description |
|
|
|
if [[ "${exif_description}" != "-" ]]; then |
|
|
|
zik_warn "description is set for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check for no disc number |
|
|
|
if [[ "${exif_disc_number}" != "-" ]]; then |
|
|
|
zik_warn "disc number is set for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
;; |
|
|
|
MP3) |
|
|
|
# Check band name |
|
|
|
if [[ "${exif_band}" != "${exif_artist}" ]]; then |
|
|
|
zik_warn "artist and band are different for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check track number |
|
|
|
local -r mp3_track_number="$(echo "${exif_track}" | cut -d '/' -f 1)" |
|
|
|
if [[ "${track_num}" != "${mp3_track_number}" ]]; then |
|
|
|
zik_warn "bad track number for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check track total |
|
|
|
local -r mp3_track_total="$(echo "${exif_track}" | cut -d '/' -f 2)" |
|
|
|
if [[ "${track_total}" != "${mp3_track_total}" ]]; then |
|
|
|
zik_warn "bad track total for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check for no comment |
|
|
|
if [[ "${exif_comment}" != "-" ]]; then |
|
|
|
zik_warn "comment is set for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check for no part of set |
|
|
|
if [[ "${exif_part_of_set}" != "-" ]]; then |
|
|
|
zik_warn "partOfSet is set for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
;; |
|
|
|
OGG) |
|
|
|
# Check album artist |
|
|
|
if [[ "${exif_album_artist}" != "${exif_artist}" ]]; then |
|
|
|
zik_warn "artist and album artist are different for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check track number |
|
|
|
if [[ "${track_num}" != "${exif_track_number}" ]]; then |
|
|
|
zik_warn "bad track number for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check track total |
|
|
|
if [[ "${track_total}" != "${exif_track_total}" ]]; then |
|
|
|
zik_warn "bad track total for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check for no description |
|
|
|
if [[ "${exif_description}" != "-" ]]; then |
|
|
|
zik_warn "description is set for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
# Check for no disc number |
|
|
|
if [[ "${exif_disc_number}" != "-" ]]; then |
|
|
|
zik_warn "disc number is set for file \"${file_path}\"" |
|
|
|
fi |
|
|
|
;; |
|
|
|
*) |
|
|
|
zik_error "unmanaged file \"${file_path}\"" |
|
|
|
;; |
|
|
|
esac |
|
|
|
} |
|
|
|
|
|
|
|
error() { |
|
|
|
echo "${0}: ${*}" >&2 |
|
|
|
} |
|
|
|
|
|
|
|
log() { |
|
|
|
echo "${@}" |
|
|
|
} |
|
|
|
|
|
|
|
on_exit() { |
|
|
|
# Remove temporary file |
|
|
|
rm -f "${EXIFTOOL_ERROR_FILE}" |
|
|
|
|
|
|
|
echo -n "Elapsed time: " |
|
|
|
if [[ "${SECONDS}" -lt 60 ]]; then |
|
|
|
echo "${SECONDS}s" |
|
|
|
elif [[ "${SECONDS}" -lt 3600 ]]; then |
|
|
|
local -r MINUTES=$(( SECONDS / 60 )) |
|
|
|
echo "${MINUTES}m$(( SECONDS % 60 ))s" |
|
|
|
else |
|
|
|
local -r HOURS=$(( SECONDS / 3600 )) |
|
|
|
local -r MINUTES=$(( SECONDS / 60 - HOURS * 60 )) |
|
|
|
echo "${HOURS}h${MINUTES}m$(( SECONDS % 60 ))s" |
|
|
|
fi |
|
|
|
} |
|
|
|
|
|
|
|
usage() { |
|
|
|
echo "Usage: ${0} [OPTIONS] PATH" |
|
|
|
echo "Validate hierarchy and tags of the music files included in PATH." |
|
|
|
echo "" |
|
|
|
echo "OPTIONS:" |
|
|
|
echo " -c COVER set cover picture file name" |
|
|
|
echo " -s SIZE set expected cover picture size" |
|
|
|
echo " -v show more informations" |
|
|
|
} |
|
|
|
|
|
|
|
zik_error() { |
|
|
|
printf "\\033[0;31m[CRIT]\\033[0m " |
|
|
|
log "${@}" |
|
|
|
} |
|
|
|
|
|
|
|
zik_info() { |
|
|
|
if [[ "${VERBOSE}" -ne 0 ]]; then |
|
|
|
printf "\\033[0;32m[INFO]\\033[0m " |
|
|
|
log "${@}" |
|
|
|
fi |
|
|
|
} |
|
|
|
|
|
|
|
zik_warn() { |
|
|
|
printf "\\033[0;33m[WARN]\\033[0m " |
|
|
|
log "${@}" |
|
|
|
} |
|
|
|
|
|
|
|
main() { |
|
|
|
# Temporary file to store exiftool error messages |
|
|
|
readonly EXIFTOOL_ERROR_FILE=$(mktemp) |
|
|
|
|
|
|
|
# Default settings |
|
|
|
local COVER_PICTURE_FILE="folder.jpg" |
|
|
|
local COVER_SIZE="500x500" |
|
|
|
local VERBOSE=0 |
|
|
|
|
|
|
|
# Process command line options |
|
|
|
while getopts ":c:s:v" flag; do |
|
|
|
case "${flag}" in |
|
|
|
c) |
|
|
|
COVER_PICTURE_FILE="${OPTARG}" |
|
|
|
;; |
|
|
|
s) |
|
|
|
COVER_SIZE="${OPTARG}" |
|
|
|
;; |
|
|
|
v) |
|
|
|
VERBOSE=1 |
|
|
|
;; |
|
|
|
\?) |
|
|
|
error "unknown option -${OPTARG}" |
|
|
|
usage |
|
|
|
exit 1 |
|
|
|
;; |
|
|
|
esac |
|
|
|
done |
|
|
|
shift "$((OPTIND-1))" |
|
|
|
|
|
|
|
# Freeze settings |
|
|
|
readonly COVER_PICTURE_FILE |
|
|
|
readonly COVER_SIZE |
|
|
|
readonly VERBOSE |
|
|
|
|
|
|
|
# Gather path to browse |
|
|
|
if [[ "${#}" -lt 1 ]]; then |
|
|
|
usage |
|
|
|
exit 1 |
|
|
|
fi |
|
|
|
local -r PATH_TO_ZIK="${1}" |
|
|
|
|
|
|
|
if [[ ! -d "${PATH_TO_ZIK}" ]]; then |
|
|
|
error "${PATH_TO_ZIK} is not a valid directory" |
|
|
|
exit 1 |
|
|
|
fi |
|
|
|
|
|
|
|
echo "--- Zik Validator ---" |
|
|
|
echo "Exiftool version: $(exiftool -ver)" |
|
|
|
|
|
|
|
for item in "${PATH_TO_ZIK}"/*; do |
|
|
|
# Remove unnecessary slashes |
|
|
|
item="$(readlink -m "${item}")" |
|
|
|
|
|
|
|
if [[ -d "${item}" ]]; then |
|
|
|
check_artist "${item}" |
|
|
|
else |
|
|
|
zik_warn "\"${item}\" is not a directory," \ |
|
|
|
"root path should contain only directories" |
|
|
|
fi |
|
|
|
done |
|
|
|
} |
|
|
|
|
|
|
|
trap on_exit EXIT |
|
|
|
SECONDS=0 |
|
|
|
main "$@" |