Quelques outils utiles pour gérer ma bibliothèque musicale
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

480 lines
15 KiB

  1. #!/bin/bash
  2. set -e
  3. set -u
  4. set -o pipefail
  5. # Include files whose name begin with a dot
  6. shopt -s dotglob
  7. # ${1} path for artist to check
  8. check_artist() {
  9. local -r artist_path="${1}"
  10. local -r artist="$(basename -- "${artist_path}")"
  11. zik_info "artist: ${artist}"
  12. # Check directory permissions
  13. if [[ "$(stat -L -c "%a" "${artist_path}")" != "755" ]]; then
  14. zik_warn "uncommon permissions for directory \"${artist_path}\""
  15. fi
  16. if [[ "${artist:0:1}" == "." ]]; then
  17. zik_error "artist name should not start with a dot (${artist})"
  18. fi
  19. if [[ -z "$(ls -A "${artist_path}")" ]]; then
  20. zik_error "no album found for artist \"${artist}\""
  21. else
  22. for item in "${artist_path}"/*; do
  23. if [[ -d "${item}" ]]; then
  24. check_album "${artist}" "${item}"
  25. else
  26. zik_warn "\"${item}\" is not a directory," \
  27. "artist path should contain only directories"
  28. fi
  29. done
  30. fi
  31. }
  32. # ${1} artist name
  33. # ${2} path for album to check
  34. check_album() {
  35. local -r artist="${1}"
  36. local -r album_path="${2}"
  37. local -r album="$(basename -- "${album_path}")"
  38. zik_info "album: ${album}"
  39. # Check directory permissions
  40. if [[ "$(stat -L -c "%a" "${album_path}")" != "755" ]]; then
  41. zik_warn "uncommon permissions for directory \"${album_path}\""
  42. fi
  43. if [[ "${album:0:1}" == "." ]]; then
  44. zik_error "album name should not start with a dot (${artist} / ${album})"
  45. fi
  46. # Check for a cover picture
  47. local -r cover_file="${album_path}/${COVER_PICTURE_FILE}"
  48. if [[ -f "${cover_file}" ]]; then
  49. zik_info "found a cover picture for ${artist} / ${album}"
  50. # Check file permissions
  51. if [[ "$(stat -L -c "%a" "${cover_file}")" != "644" ]]; then
  52. zik_warn "uncommon permissions for file \"${cover_file}\""
  53. fi
  54. # Check cover picture size
  55. local -r cover_size="$(exiftool -s3 -ImageSize "${cover_file}")"
  56. if [[ "${cover_size}" != "${COVER_SIZE}" ]]; then
  57. zik_warn "cover picture size is ${cover_size} for ${artist} / ${album}"
  58. fi
  59. else
  60. zik_warn "no cover picture \"${COVER_PICTURE_FILE}\" found" \
  61. "for ${artist} / ${album}"
  62. fi
  63. local file_name
  64. if [[ -z "$(ls -A "${album_path}")" ]]; then
  65. zik_error "no files found for ${artist} / ${album}"
  66. else
  67. # Total number of files (no cover)
  68. # shellcheck disable=SC2012
  69. local -r num="$(ls -1q -I "${COVER_PICTURE_FILE}" "${album_path}" | wc -l)"
  70. for item in "${album_path}"/*; do
  71. if [[ -f "${item}" ]]; then
  72. file_name="$(basename -- "${item}")"
  73. # Skip cover picture file
  74. if [[ "${file_name}" = "${COVER_PICTURE_FILE}" ]]; then
  75. continue
  76. fi
  77. # Check file type and name (must start with 3 digits and a dash)
  78. if [[ "${file_name##*.}" =~ ^(flac|mp3|ogg)$ ]]; then
  79. if [[ "${file_name%.*}" =~ ^[0-9]{3}\ -\ .*$ ]]; then
  80. check_file "${artist}" "${album}" "${num}" "${item}"
  81. else
  82. zik_error "badly formatted file name \"${item}\""
  83. fi
  84. else
  85. zik_error "unmanaged file \"${item}\""
  86. fi
  87. else
  88. zik_warn "\"${item}\" is not a file," \
  89. "album path should contain only files"
  90. fi
  91. done
  92. fi
  93. }
  94. # ${1} artist name
  95. # ${2} album name
  96. # ${3} number of files in album path (no cover)
  97. # ${4} path for file to check
  98. check_file() {
  99. local -r artist="${1}"
  100. local -r album="${2}"
  101. local -r track_total="$(printf "%03d" "${3}")"
  102. local -r file_path="${4}"
  103. local -r file_name="$(basename -- "${file_path}")"
  104. # Check file permissions
  105. if [[ "$(stat -L -c "%a" "${file_path}")" != "644" ]]; then
  106. zik_warn "uncommon permissions for file \"${file_path}\""
  107. fi
  108. # Extract track info from file name
  109. local -r track_num="$(echo "${file_name%.*}" | cut -d " " -f 1)"
  110. local -r track_name="$(echo "${file_name%.*}" | cut -d " " -f 3-)"
  111. local -r track_ext="${file_name##*.}"
  112. # Reset exiftool error file size
  113. truncate -s 0 "${EXIFTOOL_ERROR_FILE}"
  114. # Gather audio file tags
  115. local -a tags
  116. # shellcheck disable=SC2016
  117. readarray -t tags <<< "$(exiftool -f -p '$FileType' \
  118. -p '$FileTypeExtension' \
  119. -p '$Title' \
  120. -p '$Artist' \
  121. -p '$Band' \
  122. -p '$Album' \
  123. -p '$AlbumArtist' \
  124. -p '$Comment' \
  125. -p '$Description' \
  126. -p '$Genre' \
  127. -p '$DiscNumber' \
  128. -p '$PartOfSet' \
  129. -p '$Track' \
  130. -p '$TrackNumber' \
  131. -p '$TrackTotal' \
  132. -p '$PictureDescription' \
  133. -p '$PictureType' \
  134. -p '$Discid' \
  135. -p '$Artistsort' \
  136. -p '$Albumartistsort' \
  137. -p '$MusicbrainzAlbumid' \
  138. -p '$MusicbrainzArtistid' \
  139. -p '$MusicbrainzAlbumartistid' \
  140. -p '$MusicbrainzDiscid' \
  141. -p '$MusicbrainzTrackid' \
  142. "${file_path}" \
  143. 2> "${EXIFTOOL_ERROR_FILE}")"
  144. # Manage gathered tags
  145. local -r exif_file_type="${tags[0]}"
  146. local -r exif_file_type_extension="${tags[1]}"
  147. local -r exif_title="${tags[2]}"
  148. local -r exif_artist="${tags[3]}"
  149. local -r exif_band="${tags[4]}"
  150. local -r exif_album="${tags[5]}"
  151. local -r exif_album_artist="${tags[6]}"
  152. local -r exif_comment="${tags[7]}"
  153. local -r exif_description="${tags[8]}"
  154. local -r exif_genre="${tags[9]}"
  155. local -r exif_disc_number="${tags[10]}"
  156. local -r exif_part_of_set="${tags[11]}"
  157. local -r exif_track="${tags[12]}"
  158. local -r exif_track_number="${tags[13]}"
  159. local -r exif_track_total="${tags[14]}"
  160. local -r exif_picture_description="${tags[15]}"
  161. local -r exif_picture_type="${tags[16]}"
  162. local -r exif_disc_id="${tags[17]}"
  163. local -r exif_artist_sort="${tags[18]}"
  164. local -r exif_album_artist_sort="${tags[19]}"
  165. local -r exif_mb_album_id="${tags[20]}"
  166. local -r exif_mb_artist_id="${tags[21]}"
  167. local -r exif_mb_album_artist_id="${tags[22]}"
  168. local -r exif_mb_disc_id="${tags[23]}"
  169. local -r exif_mb_track_id="${tags[24]}"
  170. # Manage exiftool error messages
  171. if [[ -s "${EXIFTOOL_ERROR_FILE}" ]]; then
  172. zik_warn "exiftool output error for file \"${file_path}\""
  173. while read -r EXIFTOOL_MSG; do
  174. echo " ${EXIFTOOL_MSG}"
  175. done < "${EXIFTOOL_ERROR_FILE}"
  176. fi
  177. # Check type extension
  178. if [[ "${track_ext}" != "${exif_file_type_extension}" ]]; then
  179. zik_error "type extension mismatch for file \"${file_path}\""
  180. fi
  181. # Check artist
  182. if [[ "${artist}" != "${exif_artist}" ]]; then
  183. zik_warn "artist mismatch for file \"${file_path}\""
  184. fi
  185. # Check album
  186. local -r album_clean="${album//["/"" ""-""?""'"":""\"""_""*""+"]/}"
  187. local -r exif_album_clean="${exif_album//["/"" ""-""?""'"":""\"""_""*""+"]/}"
  188. if [[ "${album_clean}" != "${exif_album_clean}" ]]; then
  189. zik_warn "album mismatch for file \"${file_path}\""
  190. echo " Album from file: \"${album}\""
  191. echo " Album from tags: \"${exif_album}\""
  192. fi
  193. # Check title
  194. local -r track_name_clean="${track_name//["/"" ""-""?""'"":""\"""_""*""+"]/}"
  195. local -r exif_title_clean="${exif_title//["/"" ""-""?""'"":""\"""_""*""+"]/}"
  196. if [[ "${track_name_clean}" != "${exif_title_clean}" ]]; then
  197. zik_warn "track title mismatch for file \"${file_path}\""
  198. echo " Title from file: \"${track_name}\""
  199. echo " Title from tags: \"${exif_title}\""
  200. fi
  201. # Check genre
  202. if [[ "${exif_genre}" != "-" ]]; then
  203. zik_warn "genre is set to \"${exif_genre}\" for file \"${file_path}\""
  204. fi
  205. # Check picture file name
  206. if [[ "${exif_picture_description}" != "${COVER_PICTURE_FILE}" ]]; then
  207. zik_warn "bad embedded picture name for file \"${file_path}\""
  208. fi
  209. # Check picture type
  210. if [[ "${exif_picture_type}" != "Front Cover" ]]; then
  211. zik_warn "embedded picture is not front cover for file \"${file_path}\""
  212. fi
  213. # Check disc identifier
  214. if [[ "${exif_disc_id}" != "-" ]]; then
  215. zik_warn "disc identifier is set to \"${exif_disc_id}\" for file \"${file_path}\""
  216. fi
  217. # Check artist sort tag
  218. if [[ "${exif_artist_sort}" != "-" ]]; then
  219. zik_warn "tag Artistsort is set to \"${exif_artist_sort}\" for file \"${file_path}\""
  220. fi
  221. # Check album artist sort tag
  222. if [[ "${exif_album_artist_sort}" != "-" ]]; then
  223. zik_warn "tag Albumartistsort is set to \"${exif_album_artist_sort}\" for file \"${file_path}\""
  224. fi
  225. # Check Musicbrainz album identifier
  226. if [[ "${exif_mb_album_id}" != "-" ]]; then
  227. zik_warn "Musicbrainz album identifier is set to \"${exif_mb_album_id}\" for file \"${file_path}\""
  228. fi
  229. # Check Musicbrainz artist identifier
  230. if [[ "${exif_mb_artist_id}" != "-" ]]; then
  231. zik_warn "Musicbrainz artist identifier is set to \"${exif_mb_artist_id}\" for file \"${file_path}\""
  232. fi
  233. # Check Musicbrainz album artist identifier
  234. if [[ "${exif_mb_album_artist_id}" != "-" ]]; then
  235. zik_warn "Musicbrainz album artist identifier is set to \"${exif_mb_album_artist_id}\" for file \"${file_path}\""
  236. fi
  237. # Check Musicbrainz disc identifier
  238. if [[ "${exif_mb_disc_id}" != "-" ]]; then
  239. zik_warn "Musicbrainz disc identifier is set to \"${exif_mb_disc_id}\" for file \"${file_path}\""
  240. fi
  241. # Check Musicbrainz track identifier
  242. if [[ "${exif_mb_track_id}" != "-" ]]; then
  243. zik_warn "Musicbrainz track identifier is set to \"${exif_mb_track_id}\" for file \"${file_path}\""
  244. fi
  245. # Specific file type checks
  246. case "${exif_file_type}" in
  247. FLAC)
  248. # Check album artist
  249. if [[ "${exif_album_artist}" != "${exif_artist}" ]]; then
  250. zik_warn "artist and album artist are different for file \"${file_path}\""
  251. fi
  252. # Check track number
  253. if [[ "${track_num}" != "${exif_track_number}" ]]; then
  254. zik_warn "bad track number for file \"${file_path}\""
  255. fi
  256. # Check track total
  257. if [[ "${track_total}" != "${exif_track_total}" ]]; then
  258. zik_warn "bad track total for file \"${file_path}\""
  259. fi
  260. # Check for no description
  261. if [[ "${exif_description}" != "-" ]]; then
  262. zik_warn "description is set for file \"${file_path}\""
  263. fi
  264. # Check for no disc number
  265. if [[ "${exif_disc_number}" != "-" ]]; then
  266. zik_warn "disc number is set for file \"${file_path}\""
  267. fi
  268. ;;
  269. MP3)
  270. # Check band name
  271. if [[ "${exif_band}" != "${exif_artist}" ]]; then
  272. zik_warn "artist and band are different for file \"${file_path}\""
  273. fi
  274. # Check track number
  275. local -r mp3_track_number="$(echo "${exif_track}" | cut -d '/' -f 1)"
  276. if [[ "${track_num}" != "${mp3_track_number}" ]]; then
  277. zik_warn "bad track number for file \"${file_path}\""
  278. fi
  279. # Check track total
  280. local -r mp3_track_total="$(echo "${exif_track}" | cut -d '/' -f 2)"
  281. if [[ "${track_total}" != "${mp3_track_total}" ]]; then
  282. zik_warn "bad track total for file \"${file_path}\""
  283. fi
  284. # Check for no comment
  285. if [[ "${exif_comment}" != "-" ]]; then
  286. zik_warn "comment is set for file \"${file_path}\""
  287. fi
  288. # Check for no part of set
  289. if [[ "${exif_part_of_set}" != "-" ]]; then
  290. zik_warn "partOfSet is set for file \"${file_path}\""
  291. fi
  292. ;;
  293. OGG)
  294. # Check album artist
  295. if [[ "${exif_album_artist}" != "${exif_artist}" ]]; then
  296. zik_warn "artist and album artist are different for file \"${file_path}\""
  297. fi
  298. # Check track number
  299. if [[ "${track_num}" != "${exif_track_number}" ]]; then
  300. zik_warn "bad track number for file \"${file_path}\""
  301. fi
  302. # Check track total
  303. if [[ "${track_total}" != "${exif_track_total}" ]]; then
  304. zik_warn "bad track total for file \"${file_path}\""
  305. fi
  306. # Check for no description
  307. if [[ "${exif_description}" != "-" ]]; then
  308. zik_warn "description is set for file \"${file_path}\""
  309. fi
  310. # Check for no disc number
  311. if [[ "${exif_disc_number}" != "-" ]]; then
  312. zik_warn "disc number is set for file \"${file_path}\""
  313. fi
  314. ;;
  315. *)
  316. zik_error "unmanaged file \"${file_path}\""
  317. ;;
  318. esac
  319. }
  320. error() {
  321. echo "${0}: ${*}" >&2
  322. }
  323. log() {
  324. echo "${@}"
  325. }
  326. on_exit() {
  327. # Remove temporary file
  328. rm -f "${EXIFTOOL_ERROR_FILE}"
  329. echo -n "Elapsed time: "
  330. if [[ "${SECONDS}" -lt 60 ]]; then
  331. echo "${SECONDS}s"
  332. elif [[ "${SECONDS}" -lt 3600 ]]; then
  333. local -r MINUTES=$(( SECONDS / 60 ))
  334. echo "${MINUTES}m$(( SECONDS % 60 ))s"
  335. else
  336. local -r HOURS=$(( SECONDS / 3600 ))
  337. local -r MINUTES=$(( SECONDS / 60 - HOURS * 60 ))
  338. echo "${HOURS}h${MINUTES}m$(( SECONDS % 60 ))s"
  339. fi
  340. }
  341. usage() {
  342. echo "Usage: ${0} [OPTIONS] PATH"
  343. echo "Validate hierarchy and tags of the music files included in PATH."
  344. echo ""
  345. echo "OPTIONS:"
  346. echo " -c COVER set cover picture file name"
  347. echo " -s SIZE set expected cover picture size"
  348. echo " -v show more informations"
  349. }
  350. zik_error() {
  351. printf "\\033[0;31m[CRIT]\\033[0m "
  352. log "${@}"
  353. }
  354. zik_info() {
  355. if [[ "${VERBOSE}" -ne 0 ]]; then
  356. printf "\\033[0;32m[INFO]\\033[0m "
  357. log "${@}"
  358. fi
  359. }
  360. zik_warn() {
  361. printf "\\033[0;33m[WARN]\\033[0m "
  362. log "${@}"
  363. }
  364. main() {
  365. # Temporary file to store exiftool error messages
  366. readonly EXIFTOOL_ERROR_FILE=$(mktemp)
  367. # Default settings
  368. local COVER_PICTURE_FILE="folder.jpg"
  369. local COVER_SIZE="500x500"
  370. local VERBOSE=0
  371. # Process command line options
  372. while getopts ":c:s:v" flag; do
  373. case "${flag}" in
  374. c)
  375. COVER_PICTURE_FILE="${OPTARG}"
  376. ;;
  377. s)
  378. COVER_SIZE="${OPTARG}"
  379. ;;
  380. v)
  381. VERBOSE=1
  382. ;;
  383. \?)
  384. error "unknown option -${OPTARG}"
  385. usage
  386. exit 1
  387. ;;
  388. esac
  389. done
  390. shift "$((OPTIND-1))"
  391. # Freeze settings
  392. readonly COVER_PICTURE_FILE
  393. readonly COVER_SIZE
  394. readonly VERBOSE
  395. # Gather path to browse
  396. if [[ "${#}" -lt 1 ]]; then
  397. usage
  398. exit 1
  399. fi
  400. local -r PATH_TO_ZIK="${1}"
  401. if [[ ! -d "${PATH_TO_ZIK}" ]]; then
  402. error "${PATH_TO_ZIK} is not a valid directory"
  403. exit 1
  404. fi
  405. echo "--- Zik Validator ---"
  406. echo "Exiftool version: $(exiftool -ver)"
  407. for item in "${PATH_TO_ZIK}"/*; do
  408. # Remove unnecessary slashes
  409. item="$(readlink -m "${item}")"
  410. if [[ -d "${item}" ]]; then
  411. check_artist "${item}"
  412. else
  413. zik_warn "\"${item}\" is not a directory," \
  414. "root path should contain only directories"
  415. fi
  416. done
  417. }
  418. trap on_exit EXIT
  419. SECONDS=0
  420. main "$@"