commit 22296aa1395961952126d057fba8f6db66e23119 Author: KeksNino Date: Sat Jun 6 12:24:00 2026 +0200 add files diff --git a/2scan b/2scan new file mode 100755 index 0000000..4513117 --- /dev/null +++ b/2scan @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +dir="${1:-.}" + +find "$dir" -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.m4a" -o -iname "*.ogg" -o -iname "*.wav" -o -iname "*.aiff" \) -print0 | +xargs -0 -P"$(nproc)" -I{} sh -c ' + if ! ffprobe -v quiet -show_entries format_tags \ + -of default=nw=1:nk=1 "$1" 2>/dev/null | + grep -q -E "Visit https?://[^ ]+"; then + echo "Match found in: $1" + fi +' _ {} diff --git a/bootentry.sh b/bootentry.sh new file mode 100755 index 0000000..91ba2e8 --- /dev/null +++ b/bootentry.sh @@ -0,0 +1 @@ +sudo efibootmgr --create --disk /dev/sdd --part 1 --label "Arch Linux (Direct)" --loader '\EFI\arch\vmlinuz-linux-zen.efi' --unicode 'initrd=\EFI\arch\initramfs-linux-zen.img root=PARTUUID=19cf989f-90e6-4114-8736-ea031cb1488e iomem=relaxed pcie_acs_override=downstream,multifunction,id:10de:1c03,id:10de:10f1 rd.driver.pre=vfio-pci loglevel=3 pci-stub.ids=10de:1c03,10de:10f1 iommu=pt nvidia_drm.modeset=1 amd_iommu=on kvm.ignore_msrs=1 mitigations=off rw' diff --git a/check-playlist.sh b/check-playlist.sh new file mode 100755 index 0000000..a0af8fe --- /dev/null +++ b/check-playlist.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +PLAYLIST_DIR="/home/user/Music/Playlists/" +BASENAME="/home/user/Music/$SELECTED_SONGS" +echo $BASENAME | kitty +kitten clipboard +if grep -q "$BASENAME" $PLAYLIST_DIR/*.m3u +then + notify-send "Song is in a playlist"; +else + notify-send "Song isnt in a playlist"; +fi diff --git a/check-update b/check-update new file mode 100755 index 0000000..cd4d1a9 --- /dev/null +++ b/check-update @@ -0,0 +1,9 @@ +#!/bin/bash + +update_count=$(paru -Qu | wc -l) + +if [ $update_count -eq 0 ]; then + echo "" +else + echo " $update_count" +fi diff --git a/kernelupdate.sh b/kernelupdate.sh new file mode 100755 index 0000000..c251ed1 --- /dev/null +++ b/kernelupdate.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +sudo cp /boot/vmlinuz-linux-zen /boot/EFI/arch/vmlinuz-linux-zen.efi +sudo cp /boot/initramfs-linux-zen.img /boot/EFI/arch/initramfs-linux-zen.img +echo "done updating kernel files for efi stub" diff --git a/mpd-volume b/mpd-volume new file mode 100755 index 0000000..6edd421 --- /dev/null +++ b/mpd-volume @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +if [ $# != 1 ]; then + echo "Usage: ${0} up|down" + exit 1 +fi + +STEP=5 + +get_vol() { + mpc --host=/home/user/.config/mpd/socket volume +} + +set_vol() { + mpc --host=/home/user/.config/mpd/socket volume "$1" +} + +case "$1" in + up) + set_vol +$((STEP)) + ;; + down) + set_vol -$((STEP)) + ;; + *) + echo "Usage: ${0} up|down" + exit 1 + ;; +esac diff --git a/now-playing b/now-playing new file mode 100755 index 0000000..44a9eed --- /dev/null +++ b/now-playing @@ -0,0 +1,36 @@ +#!/bin/bash +# Can parse 'max_length=SOME_INTEGER' as argument +# (example: ./now-playing max_lentgh=40) + +default_max_length=45 + +# Function to trim the info if it exceeds 'max_length' characters +trim_info() { + local info="$1" + local max_length="$2" + + if [ ${#info} -gt $max_length ]; then + local trim_length=$((max_length - 1)) + info="${info:0:$trim_length}…" + fi + + echo "$info" +} + +# Parse the command-line argument for max_length +if [[ $1 =~ max_length=([0-9]+) ]]; then + max_length="${BASH_REMATCH[1]}" +else + max_length=$default_max_length +fi + +# Check if Spotify is playing +spotify_status=$(playerctl --player=mpd status 2>/dev/null) + +if [ "$spotify_status" == "Playing" ]; then + # Get currently playing song from Spotify + info=$(playerctl metadata --player=mpd --format ' {{title}} • {{artist}}') +fi + +trimmed_info=$(trim_info "$info" "$max_length") +echo "$trimmed_info" diff --git a/old/play-next b/old/play-next new file mode 100755 index 0000000..9e922de --- /dev/null +++ b/old/play-next @@ -0,0 +1,7 @@ +#!/bin/bash + +spotify_status=$(playerctl --player=mpd status 2>/dev/null) + +if [ "$spotify_status" == "Playing" ]; then + playerctl --player=mpd next +fi diff --git a/old/play-previous b/old/play-previous new file mode 100755 index 0000000..fee9bc3 --- /dev/null +++ b/old/play-previous @@ -0,0 +1,7 @@ +#!/bin/bash + +spotify_status=$(playerctl --player=mpd status 2>/dev/null) + +if [ "$spotify_status" == "Playing" ]; then + playerctl --player=mpd previous +fi diff --git a/old/skip30s b/old/skip30s new file mode 100755 index 0000000..1746cb1 --- /dev/null +++ b/old/skip30s @@ -0,0 +1,9 @@ +#!/bin/bash + +spotify_status=$(playerctl --player=mpd status 2>/dev/null) + +if [ "$spotify_status" == "Playing" ]; then + playerctl --player=mpd position $(($(playerctl --player=mpd position | awk '{print int($1)}') + 30)) +fi + +# mpris-ctl --player Spotify seek +30 diff --git a/old/spotify-volume.sh b/old/spotify-volume.sh new file mode 100755 index 0000000..cc9dcff --- /dev/null +++ b/old/spotify-volume.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +if [ $# != 1 ]; then + echo "Usage: ${0} up|down|mute" + exit 1 +fi + +STEP=0.03 # Volume change step + +get_vol() { + playerctl -p mpd volume +} + +set_vol() { + playerctl -p mpd volume $1 +} + +current_vol=$(get_vol) + +case $1 in + up) + new_vol=$(echo "$current_vol + $STEP" | bc) + [[ $(echo "$new_vol > 1.0" | bc) -eq 1 ]] && new_vol=1.0 + set_vol $new_vol + ;; + down) + new_vol=$(echo "$current_vol - $STEP" | bc) + [[ $(echo "$new_vol < 0.0" | bc) -eq 1 ]] && new_vol=0.0 + set_vol $new_vol + ;; + mute) + if [[ "$current_vol" != "0.0" ]]; then + set_vol 0.0 + else + set_vol 0.5 # or store last volume + fi + ;; + *) + echo "Usage: ${0} up|down|mute" + ;; +esac + diff --git a/old/spotify-volume.sh.o b/old/spotify-volume.sh.o new file mode 100755 index 0000000..0f12167 --- /dev/null +++ b/old/spotify-volume.sh.o @@ -0,0 +1,74 @@ +#!/bin/bash + + + +# check command line arguments first + +if [ $# != 1 ]; then + + echo "Usage ${0} up|down|mute" + + exit 1 + +fi + + + +if [ $1 != "up" -a $1 != "down" -a $1 != "mute" ]; then + + echo "Usage ${0} up|down|mute" + + exit 1 + +fi + + + +# go through each line of the sink-inputs output + +pactl list sink-inputs | while read line; do + + # Update current sink input id + + [[ $line =~ ^Sink\ Input\ \#([[:digit:]]+) ]] && this_input=${BASH_REMATCH[1]} + + # check if it is spotify + + if [[ $line =~ "application.name = \"spotify\"" ]]; then + + echo "Spotify is sink-input ${this_input}" + + case $1 in + + up) + + pactl set-sink-input-volume ${this_input} +5% + + ;; + + down) + + pactl set-sink-input-volume ${this_input} -5% + + ;; + + mute) + + pactl set-sink-input-mute ${this_input} toggle + + ;; + + *) + + echo "Error" + + ;; + + esac + + fi + + + +done + diff --git a/play-next b/play-next new file mode 100755 index 0000000..a99f26b --- /dev/null +++ b/play-next @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +mpc --host=/home/user/.config/mpd/socket next diff --git a/play-notify-sound b/play-notify-sound new file mode 100755 index 0000000..4eb2123 --- /dev/null +++ b/play-notify-sound @@ -0,0 +1,2 @@ +#!/bin/sh +pw-play --volume=10.0 ~/.local/share/sounds/notification.wav diff --git a/play-previous b/play-previous new file mode 100755 index 0000000..37a86fc --- /dev/null +++ b/play-previous @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +mpc --host=/home/user/.config/mpd/socket prev diff --git a/random-wall b/random-wall new file mode 100755 index 0000000..0f321c8 --- /dev/null +++ b/random-wall @@ -0,0 +1,24 @@ +#!/bin/bash + +# Directory containing wallpapers +wallpaper_dir=~/Pictures/Wallpapers + +# Function to recursively find image files +find_images() { + find "$1" -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" -o -name "*.gif" \) +} + +# Check if the wallpaper directory exists and is not empty +if [[ -d $wallpaper_dir && $(find_images "$wallpaper_dir") ]]; then + # Select a random wallpaper from all image files (including subdirectories) + random_wallpaper=$(find_images "$wallpaper_dir" | shuf -n 1) + + # Set the wallpaper using awww with transition effects + awww img "$random_wallpaper" --transition-step 100 --transition-type wipe --transition-angle 30 + + wal -i "$random_wallpaper" -n + + echo "Wallpaper set to: $random_wallpaper" +else + echo "Wallpaper directory is either missing or does not contain any images." +fi diff --git a/redoflacs b/redoflacs new file mode 100755 index 0000000..7db8544 --- /dev/null +++ b/redoflacs @@ -0,0 +1,3112 @@ +#!/usr/bin/env bash +# +# NAME +# redoflacs - BASH commandline FLAC compressor, verifier, organizer, +# analyzer, and retagger +# +# FILE INFORMATION +# Homepage: https://github.com/sirjaren/redoflacs +# Dependencies: BASH >= 4.0 +# +# DISTRIBUTION +# The MIT License +# +# Copyright 2025 Project Authors +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +############################# FUNCTIONS ############################## + +# +# Convenience functions to provide messages to the user with colored +# prefixes to indicate message severity, eg: +# _warn 'This is obsolete' +# +# Creates a messsage like (with a yellow asterisk): +# * This is obsolete +# +_info() { printf ' %s %b\n' "$green*$reset" "$@"; } # Green +_warn() { printf >&2 ' %s %b\n' "$yellow*$reset" "$@"; } # Yellow +_error(){ printf >&2 ' %s %b\n' "$red*$reset" "$@"; } # Red + +# +# Display help +# +_help() +{ + cat << HELP_EOF +Usage: redoflacs [operations] [options] [target] + +Operations: + -c Compress FLAC files + -C Compress FLAC files (even if COMPRESSION tag already exists) + -t Test integrity of FLAC files + -m Check for a valid MD5 signature in FLAC files' STREAMINFO block + -a Use auCDtect to authenticate FLAC files + -A Use auCDtect to authenticate FLAC files (create spectrogram) + -l Use LAC to authenticate FLAC files + -L Use LAC to authenticate FLAC files (create spectrogram) + -e Extract all embedded artwork from FLAC files + -p Remove metadata blocks from FLAC files (PADDING, PICTURE, etc) + -g Apply ReplayGain to FLAC files + -G Apply ReplayGain to FLAC files (even if ReplayGain tags exist) + -r Retag FLAC files preserving user-defined tags + +Options: + -j[N] Allow N jobs at once (default: all cores, or 2 if unknown) + -J[N] Allow N jobs for compression >= FLAC 1.5.0 (default: cores/threads) + -T[N] Allow N threads for compression >= FLAC 1.5.0 (default: 2) + -n Disable color output + -x Do not apply COMPRESSION tag when compressing FLAC files + -o Generate a new configuration file and exit + -v Print the version number of redoflacs and exit + -h Print this message and exit + +See redoflacs(1) for more information. +HELP_EOF +} + +# +# Display usage +# +_usage() +{ + printf >&2 ' Usage: redoflacs [operations] [options] [target] ...\n' +} + +# +# Display a message that there may have been some issues that is worth +# investigating for a given operation and where the log file to review +# said issues can be found +# +_display_issues_exist() +{ + # Operation-specific messaging + case "$operation" in + '_md5check') + _error 'The MD5 Signature is unset for some FLAC files or there were' + _error 'issues with some of the FLAC files, please check:' + ;; + '_test'|'_compress'|'_extract_images'|'_prune') + _error 'There were issues with some of the FLAC files, please check:' + ;; + '_aucdtect') + _error 'Some FLAC files may be lossy sourced, please check:' + ;; + '_replaygain') + _error 'There were issues adding ReplayGain values, please check:' + ;; + '_retag') + _error 'Some FLAC files have missing tags or there were issues with some' + _error 'of the FLAC files, please check:' + ;; + esac + + _error "${cyan}${log_file}$reset" + _error 'for details.' +} + +# +# Display a banner before script execution which shows all options +# configured as well as runtime information +# +# The structure of the banner: +# +# -------------------------------------------------------------------- +# Runtime Information +# -------------------------------------------------------------------- +# redoflacs 1.0 +# FLAC 1.5.0 +# Global Jobs 16 +# Compression Jobs 8 +# Compression Threads 2 +# -------------------------------------------------------------------- +# Configuration Options () +# -------------------------------------------------------------------- +# preserve_modtime true +# prepend_zero true +# compression_level 8 +# remove_artwork true +# skip_lossy true +# error_log +# temporary_wav_location +# spectrogram_location +# artwork_location +# -------------------------------------------------------------------- +# +# Where the width of the banner is dynamically sized to the width of the +# terminal or to the maximum length supported on script execution +# +_runtime_banner() +{ + local i option field value line align_cpu job_type + + # Generate a blue line the which matches the maximum length supported + printf -v line "%$(( MAX_LENGTH - 1 ))s" '' + line="${blue}${line// /-}$reset" + + # Replace $HOME with tilde + local config_file_title="$config_file" + config_file_title="~${config_file_title#"$HOME"}" + + # Dynamically center banner config title + local config_title="Configuration Options ($config_file_title)" + local config_title_center=$(( (MAX_LENGTH / 2) - (${#config_title} / 2) )) + + # Dynamically center banner runtime title + local runtime_title='Runtime Information' + local runtime_title_center=$(( (MAX_LENGTH / 2) - (${#runtime_title} / 2) )) + + # Dynamically center configuration and runtime options and values + # + # NOTE + # The '- 1' accounts for space between field and value + local fields_center=$(( (MAX_LENGTH / 2) - 1 )) + + # Runtime information + printf '%s\n' "$line" + printf "\033[$(_row);${runtime_title_center}H%s\n" "$runtime_title" + printf '%s\n' "$line" + + # Display centered script runtime information + printf "%${fields_center}s ${cyan}%s${reset}\n" 'redoflacs' "$script_version" + printf "%${fields_center}s ${cyan}%s${reset}\n" 'FLAC' "$flac_version" + + # Multithreaded FLAC distinguishes global jobs from jobs + [[ "$multithreaded_flac" ]] && job_type='Global Jobs' || job_type='Jobs' + + # Choose the longest character value between all jobs and threads + align_cpu="${#compress_jobs}" + (( align_cpu < ${#compress_threads} )) && align_cpu="${#compress_threads}" + (( align_cpu < ${#global_jobs} )) && align_cpu="${#global_jobs}" + + # Let the user know of possible CPU starvation if the number of global + # jobs is higher than total cores available + if [[ "$cpu_job_starvation" ]]; then + printf \ + "%${fields_center}s ${red}%-${align_cpu}s$reset ${yellow}%s$reset\n" \ + "$job_type" "$global_jobs" '(CPU Starvation Risk!)' + + # Display jobs which appear to be reasonable + else + printf "%${fields_center}s ${cyan}%s${reset}\n" "$job_type" "$global_jobs" + fi + + # Let the user know of possible CPU starvation if the number of + # compression jobs and compression threads specified are higher than + # total cores available + if [[ "$multithreaded_flac" && "$cpu_thread_starvation" ]]; then + printf \ + "%${fields_center}s ${red}%-${align_cpu}s$reset ${yellow}%s${reset}\n" \ + 'Compression Jobs' "$compress_jobs" '(CPU Starvation Risk!)' + + printf \ + "%${fields_center}s ${red}%-${align_cpu}s$reset ${yellow}%s${reset}\n" \ + 'Compression Threads' "$compress_threads" '(CPU Starvation Risk!)' + + # Display compression jobs and compression threads which appear to be + # reasonable + elif [[ "$multithreaded_flac" ]]; then + printf "%${fields_center}s ${cyan}%s${reset}\n" \ + 'Compression Jobs' "$compress_jobs" + printf "%${fields_center}s ${cyan}%s${reset}\n" \ + 'Compression Threads' "$compress_threads" + fi + + # Configuration options + printf '%s\n' "$line" + printf "\033[$(_row);${config_title_center}H%s\n" "$config_title" + printf '%s\n' "$line" + + # Display all configured options, centered + for option in "${config_options[@]}"; do + # Option field (shell variable) + field="${option%%=*}" + + # Option value (shell variable value), with a default if empty + value="${!field:-${magenta}${reset}}" + + # Eg: $HOME -> ~/ + # shellcheck disable=SC2088 + [[ "$value" == "$HOME" ]] && value='~/' + + # Eg: $HOME/user/dir -> ~/user/dir + # shellcheck disable=SC2088 + [[ "$value" == "$HOME"/* ]] && value="~/${value#"$HOME"/}/" + + # Display centered configuration option and value + printf "%${fields_center}s ${cyan}%s${reset}\n" "$field" "$value" + done + + printf '%s\n' "$line" +} + +# +# Print out current operation title +# +# The structure of an operation title: +# +# ' (<status>) (<number_of_issues>)' +# +# Which may look like: +# +# '* Verifying MD5... COMPLETED' +# '* Testing... COMPLETED 10 issues' +# '* Pruning METADATA... INTERRUPTED' +# '* Compressing To Level 8... INTERRUPTED 1 issues' +# '* Extracting Images... DID NOT RUN' +# +# Where the status may or may not be displayed and/or the number of +# issues may or may not be displayed +# +_operation_title() +{ + local fmt status_banner issues_banner right_align + + # Total number of issues found + local -i issues + issues="$(_num_issues)" + + # Ensure we do not run if running from a countdown or before an + # operation begins (only if SIGINT was sent during these times) + [[ "${FUNCNAME[1]}" == '_cleanup' && -z "$operation" ]] && return 0 + + # Generate a banner indicating total issues found so far + if (( issues > 0 )); then + issues_banner="$issues issue" + + # Add plural for more than one issue found + (( issues != 1 )) && issues_banner+='s' + fi + + # Generate an interrupted banner if the user sent SIGINT otherwise + # default to a completed status + # + # NOTE: + # The parent function will be the signal handler for SIGINT + if [[ "${FUNCNAME[1]}" == '_cleanup' ]]; then + status_banner='INTERRUPTED' + + # Add an extra space to separate issues banner if it exists + (( issues > 0 )) && status_banner+=' ' + + # Generate a completed banner if the current operation finished + # processing all items + elif (( num == ${#total_items[@]} )); then + status_banner='COMPLETED' + + # Add an extra space to separate issues banner if it exists + (( issues > 0 )) && status_banner+=' ' + fi + + # Determine where to place cursor in terminal column to right align + # possible SIGINT interruption and the number of issues to display + right_align=$(( MAX_LENGTH - ${#status_banner} - ${#issues_banner} )) + + fmt+=$"\033[${title_row}H" # Cursor row; beginning of line + fmt+=" ${green}*$reset " # Colored title prefix + fmt+='%s' # Modifier; title of operation + + # Cursor at operation title; right-aligned for issues banner + fmt+=$"\033[${title_row};${right_align}H" + + # These may not be displayed if no issues, no SIGINT, or not completed + if [[ "$status_banner" == 'INTERRUPTED'* ]]; then + fmt+="${cyan}${status_banner}$reset" # Interrupted banner + else + fmt+="${green}${status_banner}$reset" # Completed banner + fi + fmt+="${red}${issues_banner}$reset\n" # Issues banner + + # Display generated operation title message + # shellcheck disable=SC2059 + printf "$fmt" "${operation_title[$operation]}" +} + +# +# Displays the currently processed item on a dedicated row within the +# terminal +# +# The filename is truncated if the length of the filename is greater +# than 80 characters or if greater than the width of the terminal if +# that terminal width is less than 80. This width is set by 'MAX_LENGTH' +# +# Filename truncation also occurs to allow the operation label (via $1) +# and the total number of completed items to be displayed on the same +# row as the displayed item +# +# The structure of a displayed item: +# '<percent> <basename> <operation_label> <number_completed>' +# +# Which may look like: +# ' 06 - Some Amazing Song.flac Testing 6/20' +# +# This function does NOT create the display the percentage completed. +# That is handled later +# +# The file basename is left-aligned (leaving room for the percentage), +# while the operation label and total number of completed items are +# right-aligned +# +# An example of filename truncation: +# '01 - This is a long name showing truncation.flac' +# +# This filename would be displayed during an operation as: +# ' 01 - This is a long name showing truncation.fla Testing 6/20' +# +# An example of how this would look with a percentage displayed: +# ' 56% 01 - This is a long name showing truncation.fla Testing 6/20' +# +_print_item() +{ + local fmt truncated + local basename="${item##*/}" + local label="${1:+ $1}" # Operation label (with leading space) + + # The total number of items out of the total number of items, used to + # determine the maximum width these values can use, eg: + # ' 237/237' + # + # The leading space is intentional, to ensure a space is between the + # item basename and this value + local total=" ${#total_items[@]}/${#total_items[@]}" + + # This is the column position after the item basename is displayed. + # This allows the operation label and total number of completed items + # to be displayed after this column, effectively truncating the + # current item if longer than this column position + # + # NOTE: + # The item basename may be longer than the terminal width, which is + # why line wrapping is disabled and restored, to prevent the display + # of the item basename from traversing multiple lines + truncated="$(( MAX_LENGTH - ${#total} - ${#label} ))" + + # Disable line wrapping if enabled + [[ "$LINE_WRAPPING" ]] && fmt+=$'\033[?7l' + + # Belows builds the 'printf' format string + fmt+=$"\033[${row};6H" # Cursor row; 6 columns to right + fmt+=$"\033[0K" # Erase line from cursor to right + fmt+='%s' # Modifier; Current item basename + fmt+=$"\033[${row};${truncated}H" # Cursor row; Move to allow items + fmt+=$"\033[0K" # Erase line from cursor to right + fmt+="${magenta}%s" # Modifier; Operation label + fmt+="${cyan}%${#total}s${reset}" # Modifier; Items, right-aligned + + # Restore line wrapping if enabled + [[ "$LINE_WRAPPING" ]] && fmt+=$'\033[?7h' + + # Displays formatted, currently processed item + # shellcheck disable=SC2059 + printf "$fmt" "$basename" "$label" "${num}/${#total_items[@]}" +} + +# +# These functions are used after '_print_item()' to update/indicate the +# status of a given item in an operation +# +_print_item_ok() { printf '%b' "\033[${row}H${green}100%$reset"; } +_print_item_fail() { printf '%b' "\033[${row}H${red}fail$reset"; } +_print_item_check(){ printf '%b' "\033[${row}H${yellow}chck$reset"; } +_print_item_half() { printf '%b' "\033[${row}H$yellow 50%$reset"; } +_print_item_skip() { printf '%b' "\033[${row}H${cyan}skip$reset"; } + +# +# Manage termination and cleanup of this script +# +# The signal processed by this function is via $exit_code, which, if +# non-zero, is from a trap capturing a termination signal +# +# Abnormal signals caught will perform cleanup by removing files and +# killing any jobs which may be running +# +# On script end, any operation that was not run is displayed, eg: +# +# '* Testing... COMPLETED 10 issues' +# '* Compressing To Level 8... DID NOT RUN' +# '* Retagging... DID NOT RUN' +# '* Pruning METADATA... DID NOT RUN' +# +_cleanup() +{ + local i fmt op match status_banner right_align + + # Total number of issues found + local -i issues + issues="$(_num_issues)" + + # If this global variable is non-zero, a trap caught an abnormal + # signal and redefined it to an exit code value matching the signal + # received + if (( exit_code != 0 )); then + _operation_title # Display operation was interrupted + _kill_jobs "$(jobs -rp)" # Kill any running jobs + + # Clear all the operation lines by moving up each line and clearing + # it until we are just below the operation's title message + for (( i=1; i<=items_processed; i++ )); do + printf '%b' "\033[$(( post_row - i ))H\033[0K" + done + + # Remove temporary WAV and FLAC files + rm -rf "${temporary_wav_location_sub_dir:-/dev/null/foo}" + rm -f "${directory:-/dev/null/foo}"/**/*_redoflacs_$$.wav + rm -f "${directory:-/dev/null/foo}"/**/*.tmp,fl-ac+en\'c + + # Remove temporary picture blocks (from artwork extraction) + rm -f /tmp/redoflacs_picture_blocks.[[:digit:]]* + fi + + # Due to catching an abnormal signal or if there were issues found, + # loop through all operations and display any operation which did not + # run + for op in "${operations[@]}"; do + # Matched operation is either interrupted or completed with issues + [[ "$op" == "$operation" ]] && match='true' && continue + + # Skip completed operations (without issues) + [[ ! "$match" ]] && continue + + status_banner='DID NOT RUN' + + # Determine where to place cursor in terminal column to right + # align the operation status + right_align=$(( MAX_LENGTH - ${#status_banner} )) + + fmt=" ${green}*$reset " # Colored title prefix + fmt+='%s' # Modifier; title of operation + + # Cursor at operation title; right-aligned for issues banner + fmt+=$"\033[$(_row);${right_align}H" + + # Operation status banner + fmt+="${magenta}${status_banner}$reset\n" + + # Display generated operation title message + # shellcheck disable=SC2059 + printf "$fmt" "${operation_title[$op]}" + done + + # Ensure user is notified of a log file on script interruption/exit + [[ -f "$log_file" ]] && _display_issues_exist + + # Remove temporary job FIFO and temporary errors/issues file + rm -f "${job_fifo:-/dev/null/foo}" + rm -f "${issue_ticks:-/dev/null/foo}" + + # Restore terminal cursor + printf '\033[?25h' + + # Issues found without an abnormal signal + (( exit_code == 0 && issues > 0 )) && exit_code=1 +} + +# +# Kill any children process (obtained via $@), hiding errors and +# suppressing the shell's notification of terminated jobs +# +_kill_jobs() +{ + local pid + + for pid in "$@"; do + kill "$pid" 2>/dev/null + wait "$pid" 2>/dev/null + done +} + +# +# Return the current cursor's row position by sending the following +# control sequence to the controlling terminal: +# CSI 6 n # Query cursor position +# +# Which reports/returns the cursor position/terminal size as: +# CSI height ; width R # eg. '\033[80;23R' +# +_row() +{ + local -i row + + # Send CSI escape to (and read from) STDERR to not pollute STDIN + IFS='[;' read -rsu2 -d'R' -t1 -p $'\033[6n' _ row _ + + printf '%d' "${row:-0}" # Default to 0, if failure +} + +# +# Return the current cursor's column position by sending the following +# control sequence to the controlling terminal: +# CSI 6 n # Query cursor position +# +# Which reports/returns the cursor position/terminal size as: +# CSI height ; width R # eg. '\033[80;23R' +# +_col() +{ + local -i col + + # Send CSI escape to (and read from) STDERR to not pollute STDIN + IFS='[;' read -rsu2 -d'R' -t1 -p $'\033[6n' _ _ col + + printf '%d' "${col:-0}" +} + +# +# If necessary, scroll the terminal +# +_scroll_terminal() +{ + local -i row to_scroll fmt + + # Obtain current row position + row="$(_row)" + + # Default to scrolling the terminal based on the number of jobs + to_scroll="$operation_jobs" + + # Scrolling by number of items to process + (( operation_jobs > ${#total_items[@]} )) && to_scroll="${#total_items[@]}" + + # Only scroll if there are more items to display than lines available + if (( --to_scroll > (lines - row) )); then + # Scroll terminal the required number of lines + printf '%b' "\033[$(( to_scroll - (lines - row) ))S" + + # Place cursor back to where it was in relation to current output + printf '%b' "\033[$(( lines - to_scroll ))H" + fi +} + +# +# Create a configuration file +# +_create_config() +{ + local -i item=0 + + # Ensure the parent directory exists + mkdir -p "${config_file%/*}" + + # If there already is a configuration file, do not overwrite it + until [[ ! -f "$config_file" ]]; do + config_file="${HOME}/.config/redoflacs/config.$(( ++item ))" + done + + # Don't expand variables when using heredoc + cat > "$config_file" << "END_OF_CONFIG" +# +# redoflacs configuration +# +# Any line that is _NOT_ prepended with a '#' will be interpreted as an option +# (except for blank lines -- these are not interpreted) +# +#------------------------------------------------------------------------------ +# TAGGING SECTION +#------------------------------------------------------------------------------ +# +# List the tags to be kept in each FLAC file. The default is listed below. +# +# Another common tag not added by default is ALBUMARTIST. Uncomment ALBUMARTIST +# below to allow script to keep this tag. +# +# NOTE: Whitespace _IS_ allowed for the these tag fields, ie: +# ALBUM ARTIST +# CATALOG NUMBER ISBN +# +TITLE +ARTIST +#ALBUMARTIST +ALBUM +DISCNUMBER +DATE +TRACKNUMBER +TRACKTOTAL +GENRE + +# The COMPRESSION tag is a custom tag to allow the script to determine which +# level of compression the FLAC file(s) has/have been compressed at. +COMPRESSION + +# The RELEASETYPE tag is a custom tag the author of this script uses to +# catalog what kind of release the album is (ie, Full Length, EP, Demo, etc.). +RELEASETYPE + +# The SOURCE tag is a custom tag the author of this script uses to catalog +# which source the album has derived from (ie, CD, Vinyl, Digital, etc.). +SOURCE + +# The MASTERING tag is a custom tag the author of this script uses to catalog +# how the album has been mastered (ie, Lossless, or Lossy). +MASTERING + +# The REPLAYGAIN tags below, are added by the '-g' or '-G' argument. +# If you want to keep the ReplayGain tags, make sure you leave these here. +REPLAYGAIN_REFERENCE_LOUDNESS +REPLAYGAIN_TRACK_GAIN +REPLAYGAIN_TRACK_PEAK +REPLAYGAIN_ALBUM_GAIN +REPLAYGAIN_ALBUM_PEAK + +#------------------------------------------------------------------------------ +# OPTIONS +#------------------------------------------------------------------------------ +# +# The options listed below are shell syntax where option values are +# evaluated/interpolated as expected with the caveat that command substitutions +# are not allowed, eg: +# +# error_log='$HOME/logs' # $HOME/logs +# error_log='${HOME}/logs' # ${HOME}/logs +# error_log="$HOME/logs" # /home/user/logs +# error_log="${HOME}/logs" # /home/user/logs +# error_log="$(pwd)/logs" # $(pwd)/logs +# error_log="`pwd`/logs" # `pwd`/logs +# error_log="${HOME}/$(date)/logs" # ${HOME}/$(date)/logs +# error_log='${HOME}/$(date)/logs' # ${HOME}/$(date)/logs +# +# Any single quoted value and values with command substitutions are treated as +# literal strings. +# +#------------------------------------------------------------------------------ +# +# PRESERVE FILE MODIFICATION TIME: +# +# Changes whether both 'flac' and 'metaflac' programs ensure the updated files +# have the same timestamps/permissions as the input files. +# +# For 'flac' usage, this is the default, but 'metaflac' does update timestamps +# and permissions. +# +# This is enabled by setting 'preserve_modtime' option as 'true'. All other +# values are interpreted as 'false'. +# +preserve_modtime='true' + +# +# PREPEND TRACK NUMBER: +# +# Change whether the retag (-r) operation will re-tag singular track numbers +# and track totals from: +# 1, 2, 3, 4, 5, 6, 7, 8, 9 +# to +# 01, 02, 03, 04, 05, 06, 07, 08, 09 +# +# For example, if you had: +# TRACKNUMBER=4 +# TRACKTOTAL=9 +# +# You would end up with: +# TRACKNUMBER=04 +# TRACKTOTAL=09 +# +# This is enabled by setting 'prepend_zero' option as 'true'. All other values +# are interpreted as 'false'. +# +prepend_zero='false' + +# +# SET COMPRESSION: +# +# Set the type of COMPRESSION strength when compressing the FLAC files. Numbers +# range from '1-8', with '1' being the lowest compression and '8' being the +# highest compression. The default is '8'. +# +compression_level='8' + +# +# REMOVE ARTWORK: +# +# Set whether to remove embedded artwork within FLAC files. By default, this +# script will remove any artwork it can find in the PICTURE block of a FLAC +# file. Set 'remove_artwork' as 'true' to remove embedded artwork. All other +# values are intepreted as 'false'. +# +remove_artwork='true' + +# +# AUCDTECT/LAC SKIP LOSSY: +# +# Set whether FLAC files should be skipped if the MASTERING tag is already set +# as 'Lossy' when analyzed with auCDtect or LAC. Set 'skip_lossy' as 'true' to +# skip FLAC files that have the tag: 'MASTERING=Lossy'. All other values are +# intepreted as 'false'. +# +skip_lossy='true' + +# +# ERROR LOG DIRECTORY: +# +# Set where you want error logs to be placed. By default, they are stored in +# the user's HOME directory. +# +# All values for 'error_log' are interpreted as a directory. If left blank, the +# default location will be used. +# +error_log='' + +# +# WAV FILE DIRECTORY +# +# Set where to place temporary created WAV files when decoding FLAC files for +# use with auCDtect or LAC (Lossless Audio Checker). +# +# By default the WAV file is created in the same location as the FLAC file and +# removed when the operation is completed for that file. +# +# Users may prefer to place all WAV files on TMPFS backed storage or some other +# location depending on their use case. +# +# All values for 'temporary_wav_location' are interpreted as a directory. If +# left blank, the default location will be used. +# +# NOTE: +# Setting this value to a directory on a filesystem with minimal space may +# increase the chance of filesystem exhaustion if running with a large number +# of parallel jobs +# +temporary_wav_location='' + +# +# SPECTROGRAM DIRECTORY: +# +# Set where created spectrogram images should be stored. By default, they are +# stored in the same directory as the analyzed FLAC files. Each image will have +# the same name as the tested FLAC file but with an integer-based suffix to +# allow for uniqueness. The type of image created is PNG. +# +# All values for 'spectrogram_location' are interpreted as a directory. If left +# blank, the default location will be used. +# +# An example of a user-defined location: +# +# spectrogram_location="${HOME}/specs" +# +# Example spectrogram: +# +# ${HOME}/specs/music/artist/album/file.flac.spec.png.0 +# +spectrogram_location='' + +# +# EXTRACTED ARTWORK DIRECTORY: +# +# Set where the extracted artwork images should be stored. +# +# By default, each extracted image will be placed in a subdirectory where the +# FLAC file is located. The subdirectory housing the extracted artwork will +# have a similar name as the currently processed FLAC. If a directory already +# exists, an integer is appended to the directory (to prevent overwriting and +# mixing files). For example: +# +# /music/artist/album/file.flac # FLAC file with embedded images +# /music/artist/album/file.flac.art.0/ # Directory housing artwork +# /music/artist/album/file.flac.art.1/ # '1' if above directory exists +# /music/artist/album/file.flac.art.2/ # '2' if above directory exists +# +# All values for 'artwork_location' are interpreted as a directory. If left +# blank, the default location will be used. +# +# If there is a user-defined location, the extracted images will be placed in a +# subdirectory in that location with a naming scheme similar to above: +# +# artwork_location="${HOME}/artwork" # User-defined configuration option +# /music/artist/album/file.flac # FLAC file with embedded artwork +# +# # Directory housing artwork (incremented if already existing) +# ${HOME}/artwork/music/artist/album/file.flac.art.0/ +# ${HOME}/artwork/music/artist/album/file.flac.art.1/ +# ${HOME}/artwork/music/artist/album/file.flac.art.2/ +# +artwork_location='' +END_OF_CONFIG + + # Indicate if there was an existing configuration file + if [[ "$config_file" =~ ^.+[[:digit:]]$ ]]; then + _warn 'An existing configuration file is here:' + _warn "${cyan}${config_file%.*}$reset\n" + + _info 'A new configuration file was created here:' + _info "${cyan}${config_file}$reset\n" + + _info 'Ensure the new configuration file is copied here:' + _info "${cyan}${config_file%.*}$reset\n" + + _info 'In order for this program to make use of it' + + # No existing configuration file was found, indicate review of file + else + _info 'A configuration file has been created here:' + _info "${cyan}${config_file}$reset\n" + + _info 'It is recommended to review the new configuration file before' + _info 'running this program.' + fi +} + +# +# Parse the configuration file +# +_parse_config() +{ + local line option value valid_option + + # Used to check for missing, but required, configuration options + local -a missing_options + + # Used in 'flac' and 'metaflac' invocations + local -ga flac_extra_options metaflac_extra_options + + # Used in _retag() function + local -ga tags_to_keep + + # Used in script banner + local -ga config_options + + # Skip irrelevant configuration file lines + local regex_skip='^([[:space:]]+|#.*|)$' + + # Matches on configuration options (shell variables) + local regex_options='^([^[:space:]]+=.*)$' + + # Matches on possible command substitutions in option values + local regex_cmds='\$\(|`' + + # Unnecessary, as these variables are dynamically exported from the + # config but are defined to silence shellcheck of unused variables + local -g \ + preserve_modtime \ + prepend_zero \ + compression_level \ + remove_artwork \ + skip_lossy \ + error_log \ + temporary_wav_location \ + spectrogram_location \ + artwork_location + + # Key/value associative array used to evaluate valid/missing options + local -A valid_config_options + valid_config_options=( + [preserve_modtime]='true' + [prepend_zero]='true' + [compression_level]='true' + [remove_artwork]='true' + [skip_lossy]='true' + [error_log]='true' + [temporary_wav_location]='true' + [spectrogram_location]='true' + [artwork_location]='true' + ) + + # Process the configuration file, storing all user-defined tags and + # evaluate each configuration option + while IFS=$'\n' read -r line; do + # Skip empty lines, comment lines and lines with whitespace + [[ "$line" =~ $regex_skip ]] && continue + + # Non-option (shell variables) are treated as user-defined FLAC tags + if [[ ! "$line" =~ $regex_options ]]; then + # FLAC tags stored into a global array + tags_to_keep+=( "${line^^}" ) + continue + fi + + # Skip if option is not part of the valid options + [[ ! "${valid_config_options["${line%%=*}"]}" ]] && continue + + # Store configuration option key/value + config_options+=( "$line" ) + + # Break up line into shell variable and value + IFS='=' read -r option value <<< "$line" + + # Remove leading/trailing whitespace from value (if any) + IFS=$' \t\n' read -r value <<< "$value" + + # Do not interpolate value if single quotes are around value + if [[ "$value" =~ ^\'.*\'$ ]]; then + # Remove single quotes from value + value="${value#\'}" + value="${value%\'}" + + # Set configuration option and value into environment + export "$option"="$value" + continue + fi + + # Remove double quotes from value (if any) + value="${value#\"}" + value="${value%\"}" + + # Do not interpolate possible command subsitutions and only + # interpolate values which contain a variable prefix + # + # NOTE: + # BASH >= 4.4.x allows for parameter transformations which are + # safer than using 'eval' + if [[ ! "$value" =~ $regex_cmds && "$value" == *\$* ]]; then + if (( ${BASH_VERSINFO[0]}${BASH_VERSINFO[1]} >= 44 )); then + value="${value@P}" # Expand like a prompt string + else + value="$(eval "printf '%s' \"$value\"" 2>/dev/null)" + fi + fi + + # Interpolate configuration option and value into environment + export "$option"="$value" + done < "$config_file" + + # Look for any missing options in the user's configuration file and + # notify the user + for valid_option in "${!valid_config_options[@]}"; do + if [[ " ${config_options[*]%%=*} " != *" $valid_option "* ]]; then + missing_options+=( "$valid_option" ) + fi + done + + # Exit if there were any missing options from the configuration file + if [[ "${missing_options[*]}" ]]; then + _error 'The following required options are missing:' + for option in "${missing_options[@]}"; do + _error " ${cyan}${option}$reset" + done + + _error + + _error 'Recreate a configuration file (non-destructive) via:' + _error " ${cyan}redoflacs -o $reset" + exit 1 + fi + + # Handle timestamp preseveration for both 'flac' and 'metaflac' + if [[ "${preserve_modtime,,}" == 'true' ]]; then + flac_extra_options+=( '--preserve-modtime' ) + metaflac_extra_options+=( '--preserve-modtime' ) + + # Metaflac defaults to not preserving modification times and does not + # have a flag to turn preservation off + else + flac_extra_options+=( '--no-preserve-modtime' ) + fi +} + +# +# Display countdown before retagging to allow user to quit script safely +# +_countdown_metadata() +{ + # Warning message + _error "${yellow}CAUTION!${reset} These are the tag fields that will be kept" + _error 'when re-tagging the selected files:\n' + + # Creates the listing of tags to be kept + printf ' %s\n' "${tags_to_keep[@]}" + printf '\n' + + # Warning message about embedded coverart + _error "By default, this script will ${red}REMOVE${reset} the legacy" + _error "${cyan}COVERART${reset} tag.\n" + + _error "Add the ${cyan}COVERART${reset} tag to the list of tags to be kept" + _error "in the ${cyan}TAGGING SECTION${reset} of the configuration file.\n" + + _error "Keep in mind, if the ${cyan}remove_artwork${reset} option is set to" + _error "${cyan}false${reset}, embedded artwork in the ${cyan}PICTURE${reset}" + _error "block will be kept when using the ${cyan}-p, --prune${reset} option" + _error "as well.\n" + + _warn "Waiting ${red}20${reset} seconds before starting program..." + _warn 'Ctrl+C (Control-C) to abort...\n' + + printf '%b' " ${green}*${reset} Starting in: " + + # 10 second countdown + for count in {20..1}; do + printf "${red}%d ${reset}" "$count" + read -rt1 # Sleep 1 + done + printf '\n' +} + +# +# Return an integer detailing the number of issues an operation may have +# had, returning '0', if no issues were found +# +_num_issues() +{ + local ticks + + # Read in number of issue ticks, hiding missing file output + { read -r ticks < "$issue_ticks"; } 2>/dev/null + printf '%d' "${#ticks}" # Return number of ticks (0, if empty) +} + +# +# Obtain and process the positional parameters invoked with the script +# +# The following global variables are defined, should options which +# define them are specified: +# +# $create_spectrogram +# $directory +# $force_compression +# $force_replaygain +# $no_extra_tags +# $operations[@] +# +_process_arguments() +{ + # Global variables used in other functions/operations + local -g create_spectrogram force_compression force_replaygain \ + no_extra_tags directory operations global_jobs compress_jobs \ + compress_threads + + local arg invalid op + local -a conflicting operations_to_run + + # If no arguments are made to the script show usage and help + if (( $# == 0 )); then + _help + exit 1 + fi + + # Processes all the possible positional parameters, short options + # only, handling conflicting parameters + while getopts ':j:J:T:mtcCaAgLlGrepnhvox' arg "$@"; do + case "$arg" in + # Given argument is not valid. 'OPTARG' is 'arg' before 'arg' is + # set to '?' + '?') + [[ "$invalid" == *"-$OPTARG"* ]] && continue + invalid+="${invalid:+ }-$OPTARG" + ;; + + # Required parameter for a given argument was not specified + ':') + _error "Option -$OPTARG, requires a non-zero integer, eg: -${OPTARG}4" + _help + exit 1 + ;; + + # Help message and exit + 'h') _help; exit 0 ;; + + # Dipslay script version and exit + 'v') printf 'redoflacs %s\n' "$script_version"; exit 0 ;; + + # Create a new configuration file and exit + 'o') _create_config; exit 0 ;; + + # Disable color output + 'n') unset -v 'reset' 'red' 'green' 'yellow' 'blue' 'magenta' 'cyan' ;; + + # Prevent COMPRESSION tag from being applied during compression + 'x') no_extra_tags='true' ;; + + # Validate MD5 checksum in FLAC files + 'm') operations_to_run+=( '_md5check' ) ;; + + # Test FLAC files + 't') operations_to_run+=( '_test' ) ;; + + # Compression and force compression arguments + 'c'|'C') + # Skip as both compression arguments were already specified + [[ "${conflicting[*]}" == *'-c, -C'* ]] && continue + + operations_to_run+=( '_compress' ) + [[ "$arg" == 'C' ]] && force_compression='true' + + # '-C' was already specified and '-c' is the current argument + if [[ "$arg" == 'c' && "$force_compression" ]]; then + conflicting+=( '-c, -C' ) + + # '-c' was already specified and '-C' is the current argument + elif [[ "$arg" == 'C' && ! "$force_compression" ]]; then + conflicting+=( '-c, -C' ) + fi + ;; + + # auCDtect and auCDtect with spectrogram arguments + 'a'|'A') + # Skip as both auCDtect arguments were already specified + [[ "${conflicting[*]}" == *'-a, -A'* ]] && continue + + operations_to_run+=( '_aucdtect' ) + [[ "$arg" == 'A' ]] && create_spectrogram='true' + + # '-A' was already specified and '-a' is the current argument + if [[ "$arg" == 'a' && "$create_spectrogram" ]]; then + conflicting+=( '-a, -A' ) + + # '-a' was already specified and '-A' is the current argument + elif [[ "$arg" == 'A' && ! "$create_spectrogram" ]]; then + conflicting+=( '-a, -A' ) + fi + ;; + + # LAC and LAC with spectrogram arguments + 'l'|'L') + # Skip as both LAC arguments were already specified + [[ "${conflicting[*]}" == *'-l, -L'* ]] && continue + + operations_to_run+=( '_lac' ) + [[ "$arg" == 'L' ]] && create_spectrogram='true' + + # '-L' was already specified and '-l' is the current argument + if [[ "$arg" == 'l' && "$create_spectrogram" ]]; then + conflicting+=( '-l, -L' ) + + # '-l' was already specified and '-L' is the current argument + elif [[ "$arg" == 'L' && ! "$create_spectrogram" ]]; then + conflicting+=( '-l, -L' ) + fi + ;; + + # ReplayGain and force ReplayGain arguments + 'g'|'G') + # Skip as both ReplayGain arguments were already specified + [[ "${conflicting[*]}" == *'-g, -G'* ]] && continue + + operations_to_run+=( '_replaygain' ) + [[ "$arg" == 'G' ]] && force_replaygain='true' + + # '-G' was already specified and '-g' is the current argument + if [[ "$arg" == 'g' && "$force_replaygain" ]]; then + conflicting+=( '-g, -G' ) + + # '-g' was already specified and '-G' is the current argument + elif [[ "$arg" == 'G' && ! "$force_replaygain" ]]; then + conflicting+=( '-g, -G' ) + fi + ;; + + # Retag FLAC files + 'r') operations_to_run+=( '_retag' ) ;; + + # Extract embedded artwork from FLAC files + 'e') operations_to_run+=( '_extract_images' ) ;; + + # Prune unnecessary metadata blocks from FLAC files + 'p') operations_to_run+=( '_prune' ) ;; + + # Specify number of jobs/threads + 'j'|'J'|'T') + # OPTARG is the (required) number after $arg + if [[ ! "$OPTARG" =~ ^[[:digit:]]+$ ]] || (( OPTARG == 0 )); then + _error "Option -$arg, requires a non-zero integer, eg: -${arg}4" + _error "Current value applied: $OPTARG" + _help + exit 1 + fi + + # Set the relevant jobs/threads based on type of argument + [[ "$arg" == 'j' ]] && global_jobs="$OPTARG" + [[ "$arg" == 'J' ]] && compress_jobs="$OPTARG" + [[ "$arg" == 'T' ]] && compress_threads="$OPTARG" + ;; + esac + done + + # Invalid parameters were specified + if [[ "$invalid" ]]; then + _error 'Invalid arguments specified:' + _error " ${cyan}${invalid}$reset" + fi + + # Conflicting parameters were specified + if (( ${#conflicting[@]} > 0 )); then + _error 'Conflicting operations specified:' + for option in "${conflicting[@]}"; do + _error " ${cyan}${option}$reset" + done + fi + + # Display help and exit if invalid or conflicting parameters exist + if [[ "$invalid" ]] || (( ${#conflicting[@]} )); then + _help + exit 1 + fi + + # Clear current arguments processed by getopts, including the last + # non-option encountered + shift $(( OPTIND - 1 )) + + # Make sure a directory is specified (non-option argument) + if (( $# == 0 )); then + _error 'Please specify a valid directory!' + _help + exit 1 + + # Ensure only directory is specified, which also shows any invalid + # non-option arguments applied + elif (( $# > 1 )); then + _error 'Unexpected arguments:' + _error " ${cyan}$*${reset}" + _error 'All options must come before the target directory!' + _help + exit 1 + fi + + # The last element passed in is the directory to process FLAC files + directory="${1%/}" # Remove possible ending slash + + # Ensure the directory exists + if [[ ! -d "$directory" ]]; then + _error 'Please specify a valid directory!' + _help + exit 1 + fi + + # Configure the order to run operations as this script may be called + # to run one or all operations on a target directory of FLAC files + if [[ " ${operations_to_run[*]} " == *' _md5check '* ]]; then + operations+=( '_md5check' ) + fi + if [[ " ${operations_to_run[*]} " == *' _test '* ]]; then + operations+=( '_test' ) + fi + if [[ " ${operations_to_run[*]} " == *' _compress '* ]]; then + operations+=( '_compress' ) + fi + if [[ " ${operations_to_run[*]} " == *' _aucdtect '* ]]; then + operations+=( '_aucdtect' ) + fi + if [[ " ${operations_to_run[*]} " == *' _lac '* ]]; then + operations+=( '_lac' ) + fi + if [[ " ${operations_to_run[*]} " == *' _replaygain '* ]]; then + operations+=( '_replaygain' ) + fi + if [[ " ${operations_to_run[*]} " == *' _retag '* ]]; then + operations+=( '_retag' ) + fi + if [[ " ${operations_to_run[*]} " == *' _extract_images '* ]]; then + operations+=( '_extract_images' ) + fi + if [[ " ${operations_to_run[*]} " == *' _prune '* ]]; then + operations+=( '_prune' ) + fi +} + +# +# Check for missing programs required by this script +# +_check_missing_programs() +{ + local program + local -a programs=( 'rm' 'mkdir' 'mkfifo' 'metaflac' 'flac' ) + local -a missing_programs + + for program in "${programs[@]}"; do + type -P "$program" >/dev/null || missing_programs+=( "$program" ) + done + + # auCDtect binary + if [[ " ${operations[*]} " == *' _aucdtect '* ]]; then + type -P auCDtect >/dev/null || missing_programs+=( 'auCDtect' ) + fi + + # Lossless Audio Checker (LAC) binary + if [[ " ${operations[*]} " == *' _lac '* ]]; then + type -P LAC >/dev/null || missing_programs+=( 'LAC' ) + fi + + if [[ "$create_spectrogram" ]]; then + type -P sox >/dev/null || missing_programs+=( 'sox' ) + fi + + if [[ "${missing_programs[*]}" ]]; then + _error 'The following required programs were not found:' + for program in "${missing_programs[@]}"; do + _error " ${cyan}${program}$reset" + done + + _error + + # shellcheck disable=SC2016 + _error 'Ensure these programs are installed and available in $PATH' + exit 1 + fi +} + +# +# Run a given operation with a specified number of jobs +# +_run_parallel() +{ + local item # FLAC file (or directory) to process + local -i row + + # Starting cursor row to display first item and operation progress + row=$(( $(_row) - 1 )) + + # Start as many operations as specified by the number of jobs + for item in "${total_items[@]:0:$operation_jobs}"; do + (( row++, num++ )) + "$operation" & + done + + # Wait on operations if number of items is less than total jobs + (( ${#total_items[@]} <= operation_jobs )) && wait + + # Completion of an operation sends an integer (terminal row number) + # and newline to a FIFO (job manager). + # + # After each newline is read from the FIFO, another item is processed + while read -r row; do + (( num >= ${#total_items[@]} )) && break # Stop if no more FLACs + item="${total_items[num++]}" # Current item to process + "$operation" & + done <&3 # Read from FIFO + + # Wait on operations + wait +} + +# +# Sends the value of $row (integer) to the job manager FIFO which is +# from the parent function, _run_parallel() +# +# This value is where the cursor is located for the current operation +# on a given item, allowing the job manager to know where to put output +# for the next job, if any +# +# The newline is needed in order for the 'read' to finish reading the +# output +# +_send_operation_finished() +{ + printf >&3 '%d\n' "$row" +} + +# +# Decode FLAC to WAV file (used in operation functions) +# +# NOTE: +# This function will return 1 if failure was detected and no other +# operations need to be performed (to exit the parent function) +# +_decode_to_wav() +{ + local output previous error_msg + + # Basename of FLAC, splitting on whitespace since the 'read' below + # splits on whitespace (to get percentage token, error messages, etc) + local base="${item##*[ /]}" + + # The parent function's name, eg: _lac -> lac + local function_label="${FUNCNAME[1]#_}" + function_label="${function_label^^}" # lac -> LAC + + # Used in other functions/operations + local -g item_wav + + # Generate the WAV file path to be created from the current FLAC file + if [[ "$temporary_wav_location_sub_dir" ]]; then + # Eg: /tmp/tmp_redoflacs_dir.1234.1 -> + # /tmp/tmp_redoflacs_dir.1234.1/music/artist/album/01.flac + item_wav="${temporary_wav_location_sub_dir%/}/${item#/}" + + # Ensure the relative path is created in the temporary directory + mkdir -p "${item_wav%/*}" + + # Eg: /tmp/tmp_redoflacs_dir.1234.1/music/artist/album/01.flac -> + # /tmp/tmp_redoflacs_dir.1234.1/music/artist/album/01_redoflacs_1234.wav + item_wav="${item_wav%.*}_redoflacs_$$.wav" + else + # Eg: /music/artist/album/01.flac -> + # /music/artist/album/01_redoflacs_1234.wav + item_wav="${item%.*}_redoflacs_$$.wav" + fi + + _print_item 'Decoding' + + # As indicated by '[FIRST]', '[SECOND]', and '[LAST]', this is the + # order each of the statements will evaluate to 'true'. '[FIRST]' may + # never evaluate to 'true' (if the FLAC file fails to decode) + while IFS=$'\n' read -rd ' ' output; do + # [LAST] Store error message + if [[ "$error_msg" && -n "$output" ]]; then + [[ "$output" == 'state' ]] && break # End of error message + error_msg+=" $output" + continue + fi + + # [SECOND] An error occurred, storing first word + if [[ "$output" == 'ERROR' && "$previous" == *"${base}:" ]]; then + error_msg="$output" + continue + fi + + # [FIRST] FLAC decoding percentage complete + if [[ "$output" == *[[:digit:]]% ]]; then + output="${output//$'\b'}" # Remove all backspace characters + + # Remove beginning 'complete' string (from backspace characters) + printf "\033[${row}H${yellow}%4s$reset" "${output#complete}" + fi + + # Used to validate previous output is an error and not a filename + previous="$output" + done < <(flac 2>&1 "${flac_extra_options[@]}" -d "$item" -o "$item_wav") + + # Log failures if there are any errors + if [[ "$error_msg" ]]; then + _print_item_fail + + printf >> "$log_file" '%s [%s] %s\n' "$item" "$function_label" "$error_msg" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + + # Return 1 to break out of parent fuction + _send_operation_finished + return 1 + else + _print_item_ok + fi +} + +# +# Generate a spectrogram of current FLAC (used in operation functions) +# +_generate_spectrogram() +{ + local output rest_of_output spectrogram + + # The parent function's name, eg: _aucdtect -> aucdtect + local function_label="${FUNCNAME[1]#_}" + function_label="${function_label^^}" # aucdtect -> AUCDTECT + + # Determine where to put spectrograms by checking user config + if [[ "$spectrogram_location" ]]; then + # Eg: /media/specs -> /media/specs/artist/album/file.flac + spectrogram="${spectrogram_location%/}/${item#/}" + + # Ensure the relative path is created in the spectrogram location + mkdir -p "${spectrogram%/*}" + + # Eg: /media/specs/artist/album/file.flac -> + # /media/specs/artist/album/file.flac.spec.png.0 + spectrogram="${spectrogram}.spec.png.0" + else + # Eg: /music/artist/album/file.flac.spec.png.0 + spectrogram="${item}.spec.png.0" + fi + + # User requested a spectrogram, SoX is run to generate a spectrogram + # in the location as the current item or a user-defined location + _print_item 'Spectrogram' + + # Ensure we don't clobber the spectrogram, eg: + # /music/artist/album/file.flac.spec.png.0 -> + # /music/artist/album/file.flac.spec.png.1 + until [[ ! -f "$spectrogram" ]]; do + spectrogram="${spectrogram%.*}.$(( ${spectrogram##*.} + 1 ))" + done + + # SoX display progress percentage on STDERR as well as any error + # messages. Example output: + # + # Error messages: + # sox FAIL formats: can't open input file `file.wav': WAVE: RIF<...> + # sox FAIL formats: can't open input file `file.flacd': No such<...> + while IFS=$'\n' read -rd ' ' output; do + if [[ "$output" != *'In:'[[:digit:]]*'%' ]]; then + rest_of_output+=" $output" # Re-add spacing + continue + fi + + # Strip from leading string + output="${output##*In:}" # Strip leading string + output="${output%\%}" # Strip trailing percentage + output="${output%%.*}" # Strip floating point from output + + # Re-add percentage when printing + printf "\033[${row}H${yellow}%4s$reset" "$output%" + done < <( + sox 2>&1 "$item" -S -n spectrogram -o "$spectrogram" -t "$item" \ + -c '' -p 1 -z 90 -Z 0 -q 249 -w Hann -x 1800 -y 513 + ) + + # Strip leading space from generated output + IFS=' ' read -r rest_of_output <<< "$rest_of_output" + + # Log failures if there are any errors from SoX + if [[ "$rest_of_output" == 'sox FAIL'* ]]; then + _print_item_fail + printf >> "$log_file" \ + '%s [%s] %s\n' "$item" "${function_label}:Spectrogram" "$rest_of_output" + + # Log operation status as well as generated spectrogram location + else + _print_item_check + printf >> "$log_file" \ + '%s [%s] %s (%s)\n' \ + "$item" "${function_label}:Spectrogram" \ + "$lossless_check_results" "$spectrogram" + fi + + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + _send_operation_finished +} + +# +# Apply ReplayGain tags to each FLAC file in a given directory. +# Conditionally skip FLAC files if ReplayGain tags are already set. If +# there are any errors, they are logged +# +_replaygain() +{ + local output error_msg + local -a error_log files + + # Used to look for error messages and store wanted values + local regex="^(${item}/.*): (ERROR:.*)" + + _print_item 'Applying ReplayGain' + + # If a user chooses not to force apply ReplayGain tags, then each file + # is checked to ensure all ReplayGain tags are available, skipping the + # directory of files if all ReplayGain tags are already set + # + # NOTE: + # All errors are ignored here as they will be caught during + # application ReplayGain tags + if [[ ! "$force_replaygain" ]]; then + files=( "$item"/*.flac ) # Used for number of files + + # Obtain ReplayGain tags from all files in the current directory + mapfile -n0 -t replaygain_tags < <( + metaflac 2>/dev/null "${metaflac_extra_options[@]}" \ + --show-tag='REPLAYGAIN_REFERENCE_LOUDNESS' \ + --show-tag='REPLAYGAIN_TRACK_GAIN' \ + --show-tag='REPLAYGAIN_TRACK_PEAK' \ + --show-tag='REPLAYGAIN_ALBUM_GAIN' \ + --show-tag='REPLAYGAIN_ALBUM_PEAK' \ + "${files[@]}" + ) + + # All ReplayGain tags are set, so this directory can be skipped + # + # The total number of ReplayGain tags found on all files in the + # current directory must equal the total number of files found + # multiplied by 5 (the number of ReplayGain tags added to each file) + if (( ${#replaygain_tags[@]} == ${#files[@]} * 5 )); then + _print_item_skip + _send_operation_finished + return + fi + fi + + _print_item_half # Metaflac doesn't display a percentage complete + + # Since this operation applies to multiple files in a given directory, + # all error messages (per file) is stored into an array, then logged + # to a file + # + # Metaflac only displays error messages to STDERR, nothing to STDOUT + while IFS=$'\n' read -r output; do + # Any output is an error message + if [[ "$output" =~ $regex ]]; then + error_msg="${BASH_REMATCH[1]} [ReplayGain] " # Absolute filename + + # Error reading FLAC file (possibly fake), eg: + # ERROR: reading metadata, status = "FLAC__METADA ..." + if [[ "${BASH_REMATCH[2]}" == *", status = "* ]]; then + error_msg+="${BASH_REMATCH[2]%, status = *} " # Error + + # Files with differing sample rates, or problems decoding, eg: + # ERROR: sample rate of 44100 Hz does not match ... + # ERROR: during analysis (decoding file) + else + error_msg+="${BASH_REMATCH[2]}" + fi + + # All FLAC files are stored here to be logged + error_log+=( "$error_msg" ) + fi + done < <( + metaflac 2>&1 "${metaflac_extra_options[@]}" \ + --add-replay-gain "$item"/*.flac + ) + + # Log failures if there are any errors + if [[ "${error_log[0]}" ]]; then + _print_item_fail + printf >> "$log_file" '%s\n' "${error_log[@]}" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + else + _print_item_ok + fi + + _send_operation_finished +} + +# +# Compress FLAC files with user-defined compression, verifying +# integrity. Conditionally skip FLAC files if the 'COMPRESSION' tag +# exists for a given file and matches the compression level set in the +# user configuration file +# +_compress() +{ + local file_compression output previous error_msg warning_msg + + # Basename of FLAC, splitting on whitespace since the 'read' below + # splits on whitespace (to get percentage token, error messages, etc) + local base="${item##*[ /]}" + + _print_item 'Compressing' + + # If a user chooses not to force compression, then each file is + # checked for a 'COMPRESSION' tag that matches the compression level + # set in the user configuration. If the 'COMPRESSION' tag exists and + # matches what is set in the configuration, then this file is skipped + # + # NOTE: + # All errors are ignored here as they will be caught during + # compression + if [[ ! "$force_compression" ]]; then + file_compression="$( + metaflac 2>/dev/null "${metaflac_extra_options[@]}" \ + --show-tag='COMPRESSION' "$item" + )" + + # 'compression_level' is obtained from the configuration file, in + # form of 'COMPRESSION=<value>' + if + [[ -n "$file_compression" ]] \ + && (( ${file_compression##*=} == compression_level )) + then + _print_item_skip + _send_operation_finished + return + fi + fi + + # As indicated by '[FIRST]', '[SECOND]', '[THIRD]', '[FOURTH]', and + # '[LAST]', this is the order each of the statements will evaluate to + # 'true'. '[FIRST]' may never evaluate to 'true' (if the FLAC file + # fails to decode + while IFS=$'\n' read -rd ' ' output; do + # [LAST] Store error message + if [[ "$error_msg" && -n "$output" ]]; then + # End of error message + [[ "$output" == 'state' || "$output" == '"flac"' ]] && break + error_msg+=" $output" + continue + fi + + # [FOURTH] An error occurred, storing first word + if [[ "$output" == 'ERROR:' && "$previous" == *"${base}:" ]]; then + error_msg="$output" + continue + fi + + # [THIRD] Store rest of warning message + if [[ "$warning_msg" && -n "$output" ]]; then + # Stop if there is an error message and build that separately + [[ "$output" == *'ERROR:' ]] && error_msg='ERROR:' && continue + warning_msg+=" $output" + continue + fi + + # [SECOND] A warning occurred, storing first word + # + # NOTE: + # Due to 'read' using a space a delimiter, the WARNING message may + # have preceding text (word + newline) + if [[ "$output" == *'WARNING:' ]]; then + warning_msg='WARNING:' + continue + fi + + # [FIRST] FLAC compression percentage complete + if [[ "$output" == *[[:digit:]]% ]]; then + output="${output//$'\b'}" # Remove all backspace characters + + # Remove beginning ratio string (from backspace characters) + printf "\033[${row}H${yellow}%4s$reset" "${output#ratio=?.???}" + fi + + # Used to validate previous output is an error and not a filename + previous="$output" + done < <( + flac 2>&1 "${flac_extra_options[@]}" \ + -f -"$compression_level" -V "$flac_compress_threads" "$item" + ) + + # Add warning message to error message for logging + [[ "$warning_msg" ]] && error_msg="$warning_msg|$error_msg" + + # Log failures if there are any warnings and/or errors + if [[ "$error_msg" ]]; then + _print_item_fail + error_msg="${error_msg%,}" # Strip possible trailing comma + error_msg="${error_msg/$'\n'Type}" # Strip possible multiline error + printf >> "$log_file" '%s [Compress] %s\n' "$item" "$error_msg" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + _send_operation_finished + return + fi + + # Add a 'COMPRESSION' tag to the current FLAC file, to indicate which + # compression level was used when compressing (only if user did not + # specify no extra tags to be applied) + # + # NOTE: + # Errors are ignored as the FLAC file was verified during encoding + if [[ ! "$no_extra_tags" ]]; then + metaflac 2>/dev/null "${metaflac_extra_options[@]}" \ + --remove-tag='COMPRESSION' \ + --set-tag="COMPRESSION=$compression_level" "$item" + fi + + _print_item_ok + _send_operation_finished +} + +# +# Tests current FLAC file integrity, reading all output, displaying the +# current percentage complete. If there is an error that error is logged +# +_test() +{ + local output previous error_msg + + # Basename of FLAC, splitting on whitespace since the 'read' below + # splits on whitespace (to get percentage token, error messages, etc) + local base="${item##*[ /]}" + + _print_item 'Testing' + + # As indicated by '[FIRST]', '[SECOND]', and '[LAST]', this is the + # order each of the statements will evaluate to 'true'. '[FIRST]' may + # never evaluate to 'true' (if the FLAC file fails to decode) + while IFS=$'\n' read -rd ' ' output; do + # [LAST] Store error message + if [[ "$error_msg" && -n "$output" ]]; then + [[ "$output" == 'state' ]] && break # End of error message + error_msg+=" $output" + continue + fi + + # [SECOND] An error occurred, storing first word + if [[ "$output" == 'ERROR' && "$previous" == *"${base}:" ]]; then + error_msg="$output" + continue + fi + + # [FIRST] FLAC testing percentage complete + if [[ "$output" == *[[:digit:]]% ]]; then + printf "\033[${row}H${yellow}%4s$reset" "$output" + fi + + # Used to validate previous output is an error and not a filename + previous="$output" + done < <(flac 2>&1 "${flac_extra_options[@]}" -t "$item") + + # Log failures if there are any errors + if [[ "$error_msg" ]]; then + _print_item_fail + printf >> "$log_file" '%s [Test] %s\n' "$item" "$error_msg" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + else + _print_item_ok + fi + + _send_operation_finished +} + +# +# Test FLAC validity with Lossless Audio Checker (LAC) +# +_lac() +{ + local mastering output rest_of_output spectrogram + local -i count + + # Used in other functions + local -g lossless_check_results + + # Unit separator (replace newlines from LAC's output with US) + local us=$'\037' + + # Used to look for error messages and file status in LAC which is + # captured via STDOUT + # + # Example STDOUT from LAC (error message): + # + # Lossless Audio Checker 2.0.5 + # Copyright (c) 2012-2016 Julien Lacroix & Yann Prime + # http://losslessaudiochecker.com + # ================================================== + # File: invalid_file.wav + # Result: WAVE header RIFF chunk: bad Size + # + # Example STDOUT from LAC (file status): + # + # Lossless Audio Checker 2.0.5 + # Copyright (c) 2012-2016 Julien Lacroix & Yann Prime + # http://losslessaudiochecker.com + # ================================================== + # File: bad_file.wav + # Result: Upsampled + # + # Example regex capture groups obtained: + # + # Error messages: + # + # WAVE header RIFF chunk: bad Size + # WAVE header FMT chunk: not found + # WAVE header EXTENSIBLE chunk: bad Channel Mask + # WAVE header DATA chunk: bad ID + # + # Successful operations: + # + # Clean + # Transcoded + # Upscaled + # Upsampled + local regex="${us}Result: (.*)${us}$" + + # Check for a result from LAC that is NOT clean + local regex_non_clean='^(transcoded|upscaled|upsampled)' + + _print_item 'LAC' + + # Get the MASTERING tag of a FLAC file. Errors will be caught during + # decoding + mastering="$( + metaflac 2>/dev/null "${metaflac_extra_options[@]}" \ + --show-tag='MASTERING' "$item" + )" + + # Skip FLAC file if MASTERING=Lossy is set and the 'skip_lossy' + # configuration option is 'true' + if [[ "$mastering" == 'Lossy' && "$skip_lossy" == 'true' ]]; then + _print_item_skip + _send_operation_finished + return + fi + + # Decode FLAC file to WAV file as LAC does not support FLAC or decoded + # streams over a pipe + # + # NOTE: + # Break out of current function if there were failures decoding + _decode_to_wav || return + + _print_item 'LAC' + + # Runs LAC and parses out the progress bar (made up of '=') and + # generates a percentage of completion along with storing the result + # of the operation + # + # NOTE: + # Processing LAC's output is done by character where 'read' is + # reading in a single character at a time + while IFS=$'\n' read -rn1 output; do + # The first '=' found is the progress bar (max 50), which is counted + # and integer scaling division is performed to get the percentage + if [[ "$output" == '=' ]]; then + printf "\033[${row}H${yellow}%4s$reset" "$(( (++count * 100) / 50 ))%" + continue + fi + + # The rest of the output (after progress bar) is the file and result + if (( count == 50 )); then + # All newlines are null, which is replaced by a unit separator + [[ -z "$output" ]] && rest_of_output+="$us" || rest_of_output+="$output" + fi + done < <(LAC "$item_wav") + + # Remove temporary WAV file + rm -f "${item_wav:-/dev/null/foo}" + + # Looks for the result from LAC + [[ "$rest_of_output" =~ $regex ]] + + # Either 'Clean', 'Transcoded', 'Upscaled', or 'Upsampled' + lossless_check_results="${BASH_REMATCH[1]}" + + # LAC reports a clean file, continue with next item + if [[ "${lossless_check_results,,}" == 'clean' ]]; then + _print_item_ok + _send_operation_finished + return + fi + + # Log failure if not clean and no spectrogram is requested + if + [[ "${lossless_check_results,,}" =~ $regex_non_clean ]] \ + && [[ ! "$create_spectrogram" ]] + then + _print_item_check + printf >> "$log_file" \ + '%s [%s] %s\n' "$item" 'LAC' "$lossless_check_results" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + _send_operation_finished + return + fi + + # Generate a spectrogram of the current FLAC file and log the + # results of the operation along with the spectrogram location + _generate_spectrogram +} + +# +# Test FLAC validity with auCDtect +# +# NOTE: +# While not officially supported, should a user have a dynamically +# linked auCDtect binary, 'MALLOC_CHECK_' will to be set to '0', eg: +# +# MALLOC_CHECK_='0' auCDtect <rest_of_options> +# +_aucdtect() +{ + local output aucdtect_stdout aucdtect_stderr spectrogram + local -a bits_mastering + + # Used in other functions + local -g lossless_check_results + + # Used to look for error messages and file status in auCDtect which is + # captured via STDOUT + # + # Example STDOUT from auCDtect (error message): + # + # auCDtect: CD records authenticity detector, version 0.8 + # Copyright (c) 2004 Oleg Berngardt. All rights reserved. + # Copyright (c) 2004 Alexander Djourik. All rights reserved. + # error: file format is not supported + # + # Example STDOUT from auCDtect (file status): + # + # auCDtect: CD records authenticity detector, version 0.8 + # Copyright (c) 2004 Oleg Berngardt. All rights reserved. + # Copyright (c) 2004 Alexander Djourik. All rights reserved. + # ------------------------------------------------------------ + # Processing file: [01 - The Manifesto.wav] + # + # ------------------------------------------------------------ + # This track looks like CDDA with probability 85% + # + # Example regex capture groups obtained: + # + # Error messages: + # + # error: can not open file <WAV_FILE> + # error: file format is not supported + # + # Successful operations: + # + # This track looks like CDDA with probability 100% + # This track looks like CDDA with probability 49% + # This track looks like MPEG with probability 100% + # This track looks like MPEG with probability 92% + # Could not qualify the source of this track. + local regex=$'(error:.*)|Processing file.*--------.*\n(.*)$' + + _print_item 'auCDtect' + + # Get the bit depth and MASTERING tag of a FLAC file. Errors will be + # caught during decoding. Indices are: + # bits_mastering[0] = bit depth (eg: 16) + # bits_mastering[1] = MASTERING tag and value (eg: MASTERING=Lossy) + mapfile -n2 -t bits_mastering < <( + metaflac 2>/dev/null "${metaflac_extra_options[@]}" \ + --show-bps --show-tag='MASTERING' "$item" + ) + + # auCDtect does not support a bit depth greater than 16 (CD quality + # only), skipping file + # + # NOTE: + # A default value of 0 is specified in the event the bit depth could + # not be obtained. Errors will be caught during decoding + if (( ${bits_mastering[0]:-0} > 16 )); then + _print_item_skip + _send_operation_finished + return + fi + + # Skip FLAC file if MASTERING=Lossy is set and the 'skip_lossy' + # configuration option is 'true' + if [[ + "${bits_mastering[1]#*=}" == 'Lossy' + && "$skip_lossy" == 'true' + ]]; then + _print_item_skip + _send_operation_finished + return + fi + + # Decode FLAC file to WAV file as LAC does not support FLAC or decoded + # streams over a pipe + # + # NOTE: + # Break out of current function if there were failures decoding + _decode_to_wav || return + + # Runs auCDtect with medium accuracy setting (for speed): + # STDOUT: auCDtect STDOUT captured to be parsed + # STDERR: auCDtect progress percentage completed sent to FD4 + _print_item 'auCDtect:Fast' + exec 4>&1 + aucdtect_stdout="$( + MALLOC_CHECK_='0' auCDtect -m20 "$item_wav" 2> >( + while IFS=$'\n' read -rd $'\r' aucdtect_stderr; do + aucdtect_stderr="${aucdtect_stderr##*[}" + printf >&4 "\033[${row}H${yellow}%4s$reset" "${aucdtect_stderr%]}" + done + ) + )" + exec 4>&- + + # Looks for error messages and file status from auCDtect + # BASH_REMATCH[1]: auCDtect error messages + # BASH_REMATCH[2]: auCDtect file status + [[ "$aucdtect_stdout" =~ $regex ]] + + # Store auCDtect status of file + lossless_check_results="${BASH_REMATCH[2]}" + + # Log failures if there are any errors + if [[ "${BASH_REMATCH[1]}" ]]; then + _print_item_fail + printf >> "$log_file" \ + '%s [%s] %s\n' "$item" 'auCDtect' "${BASH_REMATCH[1]}" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Remove temporary WAV file + rm -f "${item_wav:-/dev/null/foo}" + + # Update operation title message with current number of issues + _operation_title + _send_operation_finished + return + + # auCDtect reports CDDA of 100%, continue with next item + elif [[ "$lossless_check_results" == *'CDDA'*'100%'* ]]; then + _print_item_ok + + # Remove temporary WAV file + rm -f "${item_wav:-/dev/null/foo}" + + _send_operation_finished + return + fi + + # Current item is not CDDA of 100%, auCDtect is run with highest + # accuracy setting: + # STDOUT: auCDtect STDOUT captured to be parsed + # STDERR: auCDtect progress percentage completed sent to FD4 + _print_item 'auCDtect:Slow' + exec 4>&1 + aucdtect_stdout="$( + MALLOC_CHECK_='0' auCDtect -m0 "$item_wav" 2> >( + while IFS=$'\n' read -rd $'\r' aucdtect_stderr; do + aucdtect_stderr="${aucdtect_stderr##*[}" + printf >&4 "\033[${row}H${yellow}%4s$reset" "${aucdtect_stderr%]}" + done + ) + )" + exec 4>&- + + # Looks for error messages and file status from auCDtect + # BASH_REMATCH[1]: auCDtect error messages + # BASH_REMATCH[2]: auCDtect file status + [[ "$aucdtect_stdout" =~ $regex ]] + + # Store auCDtect status of file (also used in other functions) + lossless_check_results="${BASH_REMATCH[2]}" + + # Log failures if there are any errors + if [[ "${BASH_REMATCH[1]}" ]]; then + _print_item_fail + printf >> "$log_file" \ + '%s [%s] %s\n' "$item" 'auCDtect' "${BASH_REMATCH[1]}" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Remove temporary WAV file + rm -f "${item_wav:-/dev/null/foo}" + + # Update operation title message with current number of issues + _operation_title + _send_operation_finished + return + fi + + # auCDtect reports CDDA of 100%, continue with next item + if [[ "$lossless_check_results" == *'CDDA'*'100%'* ]]; then + _print_item_ok + + # Remove temporary WAV file + rm -f "${item_wav:-/dev/null/foo}" + + _send_operation_finished + return + fi + + # Log failure if not CDDA with 100% and no spectrogram is requested + if + [[ "$lossless_check_results" != *'CDDA'*'100%'* ]] \ + && [[ ! "$create_spectrogram" ]] + then + _print_item_check + printf >> "$log_file" \ + '%s [%s] %s\n' "$item" 'auCDtect' "$lossless_check_results" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Remove temporary WAV file + rm -f "${item_wav:-/dev/null/foo}" + + # Update operation title message with current number of issues + _operation_title + _send_operation_finished + return + fi + + # Remove temporary WAV file + rm -f "${item_wav:-/dev/null/foo}" + + # Generate a spectrogram of the current FLAC file and log the + # results of the operation along with the spectrogram location + _generate_spectrogram +} + +# +# Check for a valid MD5 checksum in FLAC file +# +_md5check() +{ + local output error_msg md5sum + + _print_item 'Verifying MD5' + + # Metaflac will display the MD5 checksum or an error (on one line). + # These values are captured and logged, appropriately + while IFS=$'\n' read -r output; do + # Error reading FLAC file (possibly fake), eg: + # fake/fake.flac: ERROR: reading metadata, status = "FLAC ..." + if [[ "$output" == *'ERROR'* ]]; then + error_msg="${output%, status = *}" # Strip status + error_msg="ERROR${error_msg#*: ERROR}" # Strip filename + break # Stop reading lines + fi + + # This is the MD5 checksum + md5sum="$output" + done < <(metaflac 2>&1 "${metaflac_extra_options[@]}" --show-md5sum "$item") + + # Log failures if there are any errors + if [[ "$error_msg" ]]; then + _print_item_fail + printf >> "$log_file" '%s [MD5check] %s\n' "$item" "$error_msg" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + + # Possible valid FLAC, but MD5 signature is unset in STREAMINFO block + elif [[ "$md5sum" == '00000000000000000000000000000000' ]]; then + _print_item_fail + printf >> "$log_file" '%s [MD5check] %s\n' "$item" \ + 'ERROR: Unset MD5 signature (00000000000000000000000000000000)' + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + else + _print_item_ok + fi + + _send_operation_finished +} + +# +# Looks for the tags specified in the configuration file, stripping all +# other tags from a given FLAC file, and logging any tags which are +# missing +# +# Process substitution when extracting tags is used to ensure trailing +# newlines are not stripped +# +_retag() +{ + local output error_msg line extracted_tag key empty_tag tag missing_tags_str + local -i index='-1' + local -a missing_tags found_tags + + _print_item 'Retagging' + + # Extracts the first tag from the list of tags to keep and does error + # handling on this operation. Other extracted tags are processeed in + # the following loop after the error handling + # + # Metaflac will display errors on one line. These are handled first + # with the tag value obtained last (if no failures) + while IFS=$'\n' read -r output; do + # Error reading FLAC file (possibly fake), eg: + # fake/fake.flac: ERROR: reading metadata, status = "FLAC ..." + if [[ "$output" == *'ERROR'* ]]; then + error_msg="${output%, status = *}" # Strip status + error_msg="ERROR${error_msg#*: ERROR}" # Strip filename + break # Stop reading lines + fi + + # This is the extracted tag (could be empty or multiple lines) + if [[ "$extracted_tag" ]]; then + extracted_tag+=$'\n'"$output" + else + extracted_tag+="$output" + fi + done < <( + metaflac 2>&1 "${metaflac_extra_options[@]}" \ + --show-tag="${tags_to_keep[0]}" "$item" + ) + + # Log failures if there are any errors + if [[ "$error_msg" ]]; then + _print_item_fail + printf >> "$log_file" '%s [Retag] %s\n' "$item" "$error_msg" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + _send_operation_finished + return + fi + + # Look for each of the tags to keep in the current FLAC flac, + # separating each of the tags as separate indices in an array. If + # there are any missing tags, these are stored as well + for key in "${!tags_to_keep[@]}"; do + # The first tag (index=0) was already obtained during error checking + if (( key != 0 )); then + IFS='' read -rd '' extracted_tag < <( + metaflac 2>&1 "${metaflac_extra_options[@]}" \ + --show-tag="${tags_to_keep[key]}" "$item" + ) + + # Strips trailing newline due to the process substitution + # preserving trailing newlines + extracted_tag="${extracted_tag%$'\n'}" + fi + + # The tag doesn't exist in the current item + if [[ -z "$extracted_tag" ]]; then + missing_tags+=( "${tags_to_keep[key]}" ) + continue + fi + + # Processes the extracted tag, looking for multiple tags of the same + # type as well and handling multiple line values for a given tag + # value. For any valid tags, each are stored as a separate index in + # an array + # + # For any stored tag, the tag field is converted to uppercase and + # the tag values are normalized (whitespace, zero-prefixed, etc) + while IFS=$'\n' read -r line; do + # Tag exists but the value is empty. This may be a missing tag + if [[ "${line^^}" == "${tags_to_keep[key]^^}=" ]]; then + # If a previous line was an empty tag field, this means the + # previous tag was, in fact, empty + [[ "$empty_tag" ]] && missing_tags+=( "$empty_tag" ) + + # Sets the tag field as empty and is used to check any + # additional lines which may be part of this tag + empty_tag="${tags_to_keep[key]^^}" + continue + + # Tag exists and has a value (first line value only) + elif [[ "${line^^}" == "${tags_to_keep[key]^^}="* ]]; then + # Strip leading/trailing whitespace + IFS=$' \t\n' read -r line <<< "${line#*=}" + + # Convert TRACKNUMBER and TRACKTOTAL to zero-prefixed numbers if + # the 'prepend_zero' configuration option is 'true' + if [[ + ( + "${tags_to_keep[key]}" == 'TRACKNUMBER' + || "${tags_to_keep[key]}" == 'TRACKTOTAL' + ) + && "$prepend_zero" == 'true' + ]]; then + # Only modify value if the value is an integer + if [[ "$line" =~ ^[[:digit:]]+$ ]]; then + printf -v line '%02d' $(( 10#$line )) + fi + fi + + # Ensure tag field is uppercase and array index, incremented. + # The index starts at -1, so is pre-incremented to 0 before + # setting a value + found_tags[++index]="${tags_to_keep[key]^^}=$line" + continue + fi + + # Strip leading/trailing whitespace + IFS=$' \t\n' read -r line <<< "$line" + + # If this variable is set, it means the previous line was an empty + # tag field but since this point is reached, the current line is a + # part of the tag so the previous line is a valid tag. The tag is + # considered NOT empty and is saved with this line as part of the + # tag's value. An example tag where this could happen: + # + # LYRICS= + # + # This is a line for the above 'LYRICS' tag field. + # This is another line which is a part of the same tag. + # + # The first line (LYRICS=) is originally considered an empty tag, + # but the following lines are processed and added to the tag + # making this tag valid and non-empty + if [[ "$empty_tag" ]]; then + found_tags[++index]="${empty_tag}="$'\n'"$line" + empty_tag='' # Reset as the tag is considered valid + else + # Another value (separate line) for the current tag field + found_tags[index]+=$'\n'"${line}" + fi + done <<< "$extracted_tag" + done + + # Runs through the missing tags and only stores the tag as missing if + # there are no other tags found which match the tag field. This could + # happen, if there are multiple tags of the same tag field and some + # are empty, but at least one is valid, eg: + # + # DATE= + # DATE= + # DATE=2001 + # + # The DATE tag is considered missing, but when processed below, would + # be considered found and valid (the other empty DATE tags will be + # removed) + for tag in "${!missing_tags[@]}"; do + # Looks through all the valid and normalized tags found + for valid_tag in "${found_tags[@]}"; do + # Matched a tag and value for a missing tag, no longer missing + if [[ "$valid_tag" == "${missing_tags[tag]}="* ]]; then + unset -v 'missing_tags[tag]' + break + fi + done + done + + # Removes all tags from the current FLAC file and sets each found tag + # all in one operation. A single operation prevents SIGINT from + # leaving the current FLAC file without any tags + # + # Metaflac >= 1.4.3 added '--remove-all-tags-except=NAME1=NAME2...' + # which is a safer operation + if + (( + flac_major > 1 || flac_minor > 4 || (flac_minor == 4 && flac_patch >= 3) + )) + then + # Store only the tag fields and join into a '=' separated string + printf -v found_tags '%s=' "${found_tags[@]/%=*}" + + # Ensure trailing '=' is removed + metaflac >/dev/null 2>&1 "${metaflac_extra_options[@]}" \ + --remove-all-tags-except="${found_tags%=}" "$item" + else + metaflac >/dev/null 2>&1 "${metaflac_extra_options[@]}" \ + --remove-all-tags "${found_tags[@]/#/--set-tag=}" "$item" + fi + + # Logs missing tags, if any were found + if [[ "${missing_tags[*]}" ]]; then + _print_item_check + + # Generates a comma-separated list of missing tags to be logged. + # Ensures spaces in tag fields are properly handled + for tag in "${missing_tags[@]}"; do + if [[ "$missing_tags_str" ]]; then + missing_tags_str+=", $tag" + else + missing_tags_str+="$tag" + fi + done + + printf >> "$log_file" \ + '%s [Retag] Missing tags: %s\n' "$item" "$missing_tags_str" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + else + _print_item_ok + fi + + _send_operation_finished +} + +# +# Extact embedded artwork from a given FLAC +# +_extract_images() +{ + local -a artwork + local error_msg block_id art_id art_description mime img + local blocks="/tmp/redoflacs_picture_blocks.$BASHPID" + + # Ensure we don't clobber the temporary PICTURE blocks file, eg: + # /tmp/redoflacs_picture_blocks.0 -> /tmp/redoflacs_picture_blocks.1 + until [[ ! -f "$blocks" ]]; do + blocks="${blocks%.*}.$(( ${blocks##*.} + 1 ))" + done + + _print_item 'Extracting Images' + + # Retrieve PICTURE blocks from current item, to a temporary file + error_msg="$( + metaflac 2>&1 >"$blocks" "${metaflac_extra_options[@]}" \ + --list --block-type=PICTURE "$item" + )" + + # Log FLAC failure + if [[ -n "$error_msg" ]]; then + error_msg="${error_msg%, status = *}" # Strip end of error msg + error_msg="ERROR ${error_msg##*: ERROR: }" # Strip filename + + _print_item_fail + printf >> "$log_file" '%s [ExtractArtwork] %s\n' "$item" "$error_msg" + printf >> "$issue_ticks" '.' # Add one tick to total issues + rm -f "${blocks:-/dev/null/foo}" + + # Update operation title message with current number of issues + _operation_title + _send_operation_finished + return + fi + + # Skip if there were no PICTURE blocks found in current FLAC + if [[ ! -s "$blocks" ]]; then + _print_item_skip + rm -f "${blocks:-/dev/null/foo}" + _send_operation_finished + return + fi + + _print_item_half # Metaflac doesn't display a percentage complete + + # Store the values which can identify each image (in each PICTURE + # block) to be used during image extraction. The values needed are: + # 'METADATA block #<num>' -> $block_id + # ' type: <num> (<description>)' -> $art_id AND $art_description + # ' MIME type: image/jpeg' -> $mime + # + # NOTE: + # Reading the PICTURE block from a temporary file is much faster + # than reading from process substitution (there can be many PICTURE + # blocks in a single FLAC file and each image could be very large) + while IFS=$'\n' read -r; do + # Skip all the unnecessary information + [[ "$REPLY" != 'METADATA'* && "$REPLY" != ' '[tM]* ]] && continue + + if [[ "$REPLY" == 'METADATA'* ]]; then + block_id="${REPLY##*#}" + continue + fi + + # Ensure we don't use: 'type: 6 (PICTURE)' + if [[ "$REPLY" == ' type: '[^6]* ]]; then + IFS=' ' read -r _ art_id art_description <<< "${REPLY//\//-}" + art_description="${art_description//[()]}" # Strip parenthesis + art_description="${art_description// /_}" # Spaces underscores + continue + + # Done with a PICTURE block; the values can be stored and the + # variables reset + elif [[ "$REPLY" == ' MIME'* ]]; then + mime="${REPLY##*/}" # Eg: 'MIME type: image/jpeg' -> 'jpeg' + mime="${mime/jpeg/jpg}" # Eg: 'jpeg' -> 'jpg' + + # Eg: '4:3-Cover_front.jpg' + artwork+=( "${block_id}:${art_id}-${art_description}.${mime}" ) + unset -v 'block_id' 'art_id' 'art_description' 'mime' + fi + done < "$blocks" + rm -f "${blocks:-/dev/null/foo}" + + # Continue only if there was artwork to be extracted + if [[ -z "${artwork[*]}" ]]; then + _send_operation_finished + return + fi + + # Determine where to put extracted artwork by checking user config + if [[ "$artwork_location" ]]; then + # Eg: /media/images -> /media/images/artist/album/file.flac + artwork_location="${artwork_location%/}/${item#/}" + + # Ensure the relative path is created in the artwork location + mkdir -p "${artwork_location%/*}" + + # Eg: /media/images/artist/album/file.flac -> + # /media/images/artist/album/file.flac.art.0 + artwork_location="${artwork_location}.art.0" + else + # Eg: /music/artist/album/file.flac.art.0 + artwork_location="${item}.art.0" + fi + + # Ensure we don't clobber the artwork directory, eg: + # /music/artist/album/file.flac.art.0 -> + # /music/artist/album/file.flac.art.1 + until [[ ! -d "$artwork_location" ]]; do + artwork_location="${artwork_location%.*}.$(( + ${artwork_location##*.} + 1 + ))" + done + mkdir -p "$artwork_location" + + # Performs artwork extraction on the current item. Each array value: + # '<block_id>:<art_id>-<art_description>.<mime> + # + # Which looks like: + # '4:3-Cover_front.jpg' + for value in "${artwork[@]}"; do + # Eg: '/music/artist/album/file.flac.art.0/3-Cover_front.jpg.0' + img="${artwork_location}/${value#*:}.0" + block_id="${value%%:*}" # Eg: '4:3-Cover_front.jpg' -> '4' + + # Ensure we don't clobber the extracted image, eg: + # /music/artist/album/file.flac.art.0/3-Cover_front.jpg.0 -> + # /music/artist/album/file.flac.art.0/3-Cover_front.jpg.1 + until [[ ! -f "$img" ]]; do + img="${img%.*}.$(( ${img##*.} + 1 ))" + done + + # Extracts each image by METADATA block number + metaflac "${metaflac_extra_options[@]}" \ + --block-number="$block_id" --export-picture-to="$img" "$item" + done + + _print_item_ok + _send_operation_finished +} + +# +# Prune user configurable FLAC metadata from FLAC files +# +_prune() +{ + local output error_msg + + _print_item 'Pruning' + _print_item_half # Metaflac doesn't display a percentage complete + + # Remove all information but STREAMINFO,VORBIS_COMMENTs, and + # possibly METADATA_BLOCK_PICTURE + while IFS=$'\n' read -r output; do + # Error reading FLAC file (possibly fake), eg: + # fake/fake.flac: ERROR: reading metadata, status = "FLAC ..." + if [[ "$output" == *'ERROR'* ]]; then + error_msg="${output%, status = *}" # Strip status + error_msg="ERROR${error_msg#*: ERROR}" # Strip filename + break # Stop reading lines + fi + done < <( + metaflac 2>&1 "${metaflac_extra_options[@]}" \ + --remove --dont-use-padding \ + --except-block-type="$dont_prune_flac_metadata" "$item" + ) + + # Log failures if there are any errors + if [[ "$error_msg" ]]; then + _print_item_fail + printf >> "$log_file" '%s [Prune] %s\n' "$item" "$error_msg" + printf >> "$issue_ticks" '.' # Add one tick to total issues + + # Update operation title message with current number of issues + _operation_title + else + _print_item_ok + fi + + _send_operation_finished +} + +######################### PRE-SCRIPT CHECKS ########################## + +# For colored output +reset=$'\033[0m' +red=$'\033[31m' +green=$'\033[32m' +yellow=$'\033[33m' +blue=$'\033[34m' +magenta=$'\033[35m' +cyan=$'\033[36m' + +script_version='1.1.0' # Redoflacs version + +# +# Make sure we are running BASH 4 or greater +# +if (( BASH_VERSINFO[0] < 4 )); then + _error "You must be running ${cyan}BASH 4$reset or greater to use this" + _error 'program! Current version:' + _error " ${cyan}${BASH_VERSION}$reset" + exit 1 +fi + +# BASH 4: Used to recursively find (glob) FLAC files (eg, /**/*.flac) +shopt -s globstar 2>/dev/null + +# BASH 2: Make all globbing case-insensitive +shopt -s nocaseglob 2>/dev/null + +# +# Redoflacs configuration file +# +config_file="${HOME}/.config/redoflacs/config" + +# +# Create a new configuration file if one does not exist +# +if [[ ! -f "$config_file" ]]; then + _create_config + exit 0 +else + _parse_config +fi + +# +# Process/validate script positional parameters. This function provides +# variables which are used by later functions/operations (based on +# whether certain operations are specified) +# +_process_arguments "$@" + +# +# Check for missing programs required by this script +# +_check_missing_programs + +# +# Manage any user-defined directories from the configuration checking +# for directory existence and if writable +# +# NOTE: +# Configuration keys are passed in by name, allowing reference to +# derive the key name and indirection for the key value +# +for dir in \ + error_log \ + temporary_wav_location \ + artwork_location \ + spectrogram_location +do + # Skip any default (empty) configuration values + [[ "${!dir}" ]] || continue + + # Check for existence + if [[ ! -d "${!dir}" ]]; then + _error "${cyan}${!dir}$reset does NOT exist!" + _error + + # Check for writable + elif [[ ! -w "${!dir}" ]]; then + _error "${cyan}${!dir}$reset is NOT writable!" + _error + fi + + # Rest of error messaging regardless of type of failure + if [[ ! -d "${!dir}" || ! -w "${!dir}" ]]; then + _error "Ensure a valid value for ${cyan}${dir}$reset is used" + _error 'in the following configuration file:' + _error " ${cyan}${config_file}$reset" + exit 1 + fi +done + +# +# Generate log file location +# +# Ensure we don't overwrite an existing log file +item=0 +log_file="${error_log:-${HOME}}/redoflacs_$$.log" +until [[ ! -f "$log_file" ]]; do + log_file="${error_log:-${HOME}}/redoflacs_$$.$(( ++item )).log" +done + +# +# Generate a unique temporary directory to house temporary WAV files if +# a user specified a temporary directory +# +# Ensure we don't clobber the temporary directory, eg: +# /tmp/tmp_redoflacs_dir.1234.0 -> /tmp/tmp_redoflacs_dir.1234.1 +# +# NOTE: +# This directory is only created if an operation will decode FLAC +# files to WAV files +# +if [[ "$temporary_wav_location" ]]; then + user_tmp_dir="tmp_redoflacs_dir.$$.0" + until [[ ! -d "${temporary_wav_location%/}/$user_tmp_dir" ]]; do + user_tmp_dir="${user_tmp_dir%.*}.$(( ${user_tmp_dir##*.} + 1 ))" + done + + # Set the user temporary directory to a unique subdirectory to allow + # easy removal after script completion/termination + temporary_wav_location_sub_dir="${temporary_wav_location%/}/$user_tmp_dir" +fi + +############################### MAIN ################################# + +export LANG=C +export LC_ALL=C + +# Used in _cleanup() (via trap) to determine if an abnormal signal +# caught or there generalized (logged) failures caught by the script +exit_code=0 + +# Manage cleanup on interruption and script exit +trap 'exit_code=130; exit' SIGINT +trap 'exit_code=143; exit' SIGTERM +trap '_cleanup; exit $exit_code' EXIT + +# Hide cursor +printf '\033[?25l' + +job_fifo="/tmp/redoflacs_fifo_$$" # Job manager FIFO +issue_ticks="/tmp/redoflacs_issue_file_$$" # Issues/Errors file + +# Set FLAC metadata BLOCKS to NOT remove, used when pruning FLAC files +if [[ "$remove_artwork" == 'true' ]]; then + # Remove PICTURE block + dont_prune_flac_metadata='STREAMINFO,VORBIS_COMMENT' +else + # Do not remove PICTURE block + dont_prune_flac_metadata='STREAMINFO,PICTURE,VORBIS_COMMENT' +fi + +# +# Obtain FLAC version (used in banner) +# +flac_version="$(flac --version)" # eg: flac 1.4.3 +flac_version="${flac_version##* }" # eg: 1.4.3 + +# +# Break out FLAC version string to major, minor and patch +# +IFS='.' read -r flac_major flac_minor flac_patch <<< "$flac_version" + +# +# Determine the number of cores available, defaulting to 2 +# +cores='' +cores="${cores:-"$(command -p nproc 2>/dev/null)"}" +cores="${cores:-"$(command -p getconf _NPROCESSORS_ONLN 2>/dev/null)"}" +cores="${cores:-"$(command -p sysctl -n hw.ncpu 2>/dev/null)"}" +cores="${cores:-2}" + +# +# FLAC 1.5.0 introduced support for multithreaded encoding of FLAC files +# which is checked to support multithreaded usage in compression +# operations +# +if (( flac_major > 1 || flac_minor >= 5 )); then + # FLAC version does support multithreaded encoding + multithreaded_flac='true' + + # Compression jobs default to global jobs if global jobs is specified + # but no compression jobs were specified + [[ "$global_jobs" && ! "$compress_jobs" ]] && compress_jobs="$global_jobs" + + # Default to number of cores for all other jobs (2, if cores is + # undetermined) + global_jobs="${global_jobs:-$cores}" + + # Default to 2 compression threads + compress_threads="${compress_threads:-2}" + + # Default to half as many jobs as threads when compresssing + compress_jobs="${compress_jobs:-$(( cores / compress_threads ))}" + + # Sent to 'flac' during compression (encoding) + flac_compress_threads="--threads=$compress_threads" + + # Check for CPU starvation with compression jobs * threads + cpu_thread_starvation='' + if (( (compress_jobs * compress_threads) > cores )); then + cpu_thread_starvation='true' + fi + + # Check for CPU starvation in user-defined global jobs + cpu_job_starvation='' + (( global_jobs > cores )) && cpu_job_starvation='true' +else + # FLAC version does not support multithreaded encoding + multithreaded_flac='' + + # Default to number of cores for all jobs (2, if cores is + # undetermined) + global_jobs="${global_jobs:-$cores}" + + # FLAC < 1.5.0 does not support more than one thread + compress_threads=1 + flac_compress_threads='' + cpu_thread_starvation='' + + # Check for CPU starvation in user-defined global jobs + cpu_job_starvation='' + (( global_jobs > cores )) && cpu_job_starvation='true' +fi + +# +# These are operation title messages which are used by +# _operation_title() to dynamically display a title via key lookup +# +declare -A operation_title +operation_title['_md5check']='Verifying MD5...' +operation_title['_test']='Testing...' +operation_title['_compress']="Compressing To Level ${compression_level}..." +operation_title['_aucdtect']='auCDtect Validation...' +operation_title['_lac']='Lossless Audio Checker (LAC) Validation...' +operation_title['_replaygain']='Applying ReplayGain...' +operation_title['_retag']='Retagging...' +operation_title['_extract_images']='Extracting Images...' +operation_title['_prune']='Pruning METADATA...' + +# +# Obtain the total number of lines and columns available in the terminal +# by sending the following control sequences to the terminal driver: +# +# CSI s # Save cursor position +# CSI 999999 999999 H # Move cursor to the end of the last row +# CSI 6 n # Query cursor position +# CSI u # Restore cursor position +# +# 'CSI 6 n' reports/returns the cursor position/terminal size as: +# +# CSI height ; width R +# +# An example: +# +# '\033[80;23R' +# +# Send CSI escape to (and read from) STDERR to not pollute STDIN +IFS='[;' \ + read -rsu2 -d'R' -t1 -p $'\033[s\033[999999;999999H\033[6n\033[u' \ + _ lines columns + +# +# The terminal uses 1-based column indexing (columns start at 1), but +# shell arithmetic is 0-based +# +# To right-align text without manual '+ 1' adjustments in every +# calculation, MAX_LENGTH is set to '$columns + 1' +# +if (( columns < 80 )); then + readonly MAX_LENGTH="$(( columns + 1 ))" +else + readonly MAX_LENGTH='81' +fi + +# +# Print a long blank line that is one column higher than the total +# columns in the termninal to test for line wrapping +# +printf "%$(( columns + 1 ))s" '' + +# +# If the current cursor column is equal to the total columns in the +# terminal (clamping) then line wrapping is disabled +# +# If the current cursor column is greater than the total columns in the +# terminal (non-clamping) then line wrapping is also disabled +# +if (( $(_col) >= columns )); then + readonly LINE_WRAPPING='' + + # Move back to column 0 + printf '\r' +else + readonly LINE_WRAPPING='true' + + # Move back up to the previous row at column 0 + printf '\r\033[1A' +fi + +# Display a banner for runtime information and configuration settings +_runtime_banner + +# _retag(): Display a countdown +[[ " ${operations[*]} " == *' _retag '* ]] && _countdown_metadata + +# +# Obtain the total FLAC files to process (files and/or directories) +# +# NOTE: +# The array of FLAC files/directories contain absolute pathnames which +# allows logging to determine where each item resides +# +_info 'Looking for FLAC files to process...' +pushd "$directory" >/dev/null || exit +total_items=( "${PWD}"/**/*.flac ) +popd >/dev/null || exit + +# +# If there were no FLAC files found, the glob will exist as index 0, eg: +# +# /music/artist/album/files/**/*.[Ff][Ll][Aa][Cc] +# +if [[ ! -f "${total_items[0]}" ]]; then + found_flacs_msg='NOT FOUND' + + # Right align message + found_flacs_align=$(( MAX_LENGTH - ${#found_flacs_msg} )) + + # Update title message with right aligned message indicating there + # were no FLAC files found + printf "\033[$(( $(_row) - 1 ));${found_flacs_align}H${red}%s${reset}\n" \ + "$found_flacs_msg" + + _error 'There were no FLAC files found!' + exit 1 +else + found_flacs_msg='OK' + + # Right align message + found_flacs_align=$(( MAX_LENGTH - ${#found_flacs_msg} )) + + # Update title message with right aligned message indicating success + # in finding FLAC files to process + printf "\033[$(( $(_row) - 1 ));${found_flacs_align}H${green}%s${reset}\n" \ + "$found_flacs_msg" +fi + +# +# Run through all the operations specified by the user on the provided +# FLAC files directory +# +for operation in "${operations[@]}"; do + # Used to determine current item number being processed and used by + # the operation title function + num=0 + + # Use compression jobs (tied with threads) for compression operation + if [[ "$multithreaded_flac" && "$operation" == '_compress' ]]; then + operation_jobs="$compress_jobs" + else + operation_jobs="$global_jobs" + fi + + # ReplayGain application requires operating on a directory of files + # instead of each file, individually + if [[ "$operation" == '_replaygain' ]]; then + # Backup all known FLAC files to process + total_items_backup=( "${total_items[@]}" ) + + # Obtain all the parent directories for each FLAC file (album) and + # store each album directory once into an array to apply ReplayGain + # on a directory of files instead of individual files + current='' + total_items=() + for dir in "${total_items_backup[@]%/*}"; do + previous="$current" + current="$dir" + + # Non-matching indicates a "new" directory to store + [[ "$previous" != "$current" ]] && total_items+=( "$current" ) + done + fi + + # Display title message of operation + title_row="$(_row)" + _operation_title + + # Scroll terminal up (if needed) + _scroll_terminal + title_row=$(( $(_row) - 1 )) + + # Clear job manager file descriptor (tied to FIFO) + exec 3<&- 3>&- # Close file descriptor + + # Remove FIFO if it exists + rm -f "${job_fifo:-/dev/null/foo}" + + mkfifo "$job_fifo" # Create FIFO + exec 3<>"$job_fifo" # Open file descriptor read/write + + # If the items to process are less than the number of jobs, add the + # number of items to the current row position, else add the number + # of jobs to run + if (( ${#total_items[@]} < operation_jobs )); then + items_processed="${#total_items[@]}" + else + items_processed="$operation_jobs" + fi + + post_row=$(( $(_row) + items_processed )) # Operation row position + + # Run current operation with optional parallel jobs + _run_parallel + + # Clear all the operation lines by moving up each line and clearing it + # until we are just below the operation's title message + for (( i=1; i<=items_processed; i++ )); do + printf '%b' "\033[$(( post_row - i ))H\033[0K" + done + + # Display completed operation + _operation_title + + # Break out if there were any issues found in the current operation + # + # NOTE: + # _cleanup() is run after script exit + (( $(_num_issues) > 0 )) && break + + # Restore the items to process from directories to FLAC files after + # applying ReplayGain + if [[ "$operation" == '_replaygain' ]]; then + total_items=( "${total_items_backup[@]}" ) + fi +done diff --git a/scan b/scan new file mode 100755 index 0000000..7751bd6 --- /dev/null +++ b/scan @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +dir="${1:-.}" + +find "$dir" -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.m4a" -o -iname "*.ogg" -o -iname "*.wav" -o -iname "*.aiff" \) -print0 | +xargs -0 -P"$(nproc)" -I{} bash -c ' + if ffprobe -v quiet -show_entries format_tags \ + -of default=nw=1:nk=1 "$1" 2>/dev/null | + grep -q -E "Visit https?://[^ ]+"; then + echo "Match found in: $1" + fi +' _ {} | sort diff --git a/skip30s b/skip30s new file mode 100755 index 0000000..ca42648 --- /dev/null +++ b/skip30s @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +mpc --host=/home/user/.config/mpd/socket seekthrough +30 diff --git a/spotify-pause b/spotify-pause new file mode 100755 index 0000000..58c4847 --- /dev/null +++ b/spotify-pause @@ -0,0 +1,9 @@ +#!/bin/bash + +mpd_status=$(playerctl --player=mpd status 2>/dev/null) + +if [ "$mpd_status" == "Playing" ]; then + playerctl --player=mpd pause +else + playerctl --player=mpd play 2>/dev/null || playerctl --player=spotify play +fi diff --git a/walld b/walld new file mode 100755 index 0000000..967fe4a --- /dev/null +++ b/walld @@ -0,0 +1,6 @@ +#!/bin/sh + +awww query +if [ $? -eq 1 ] ; then + awww-daemon +fi diff --git a/wallpaper-hook.sh b/wallpaper-hook.sh new file mode 100755 index 0000000..8635c76 --- /dev/null +++ b/wallpaper-hook.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +HOOK_NAME="$1" +HOOK_VALUE="$2" + +# Exit if no wallpaper path was provided +[ -z "$HOOK_VALUE" ] && exit 0 + +# Run pywal without reloading terminals +/usr/bin/wal -i "$HOOK_VALUE" -n 2>/dev/null +# /usr/bin/awww img "$HOOK_VALUE" 2>/dev/null