diff --git a/README.MD b/README.MD index 83ec5f5..cb0869a 100644 --- a/README.MD +++ b/README.MD @@ -1,5 +1,5 @@ # Bash Music sync -(c) 2018 Jeroen De Meerleer +(c) 2018-2019 Jeroen De Meerleer Script to sync music from one folder to another @@ -12,14 +12,34 @@ music-sync Syncronises music from one folder to another. Options: - -s, --source The source folder of the music - -d, --dest The destionation folder of the music - -v, --verbose Enable verbose output - -h, --help Display this help text + -s, --source The source folder of the music + -d, --dest The destination folder of the music + -t, --temp The temporary cache for converted files (default: /tmp/converted) + -c, --convert Convert files to a given bitrate in kbps before syncing (default: 192) + -a, --resize-art Resize album-art before syncing (default width: 200) + -v, --verbose <0-6> Set log level (default: 2) + -h, --help Display this help text + +Log levels: + 0 | Verbose + 1 | Debug + 2 | Info + 3 | Warning + 4 | Error + 5 | Fatal + 6 | Silence + +Exit Codes: + 1 Dependencies not met + 2 Invalid Argument + 3 Source Unreachable + 4 Destination Unreachable + 5 Command failed + ``` ## Licence -Copyright 2018 Jeroen De Meerleer +Copyright 2018-2019 Jeroen De Meerleer 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: diff --git a/music-sync.sh b/music-sync.sh index ff117a8..0b71f64 100755 --- a/music-sync.sh +++ b/music-sync.sh @@ -1,29 +1,67 @@ #!/bin/bash +begin=$(date +"%s") # saner programming env: these switches turn some bugs into errors set -o errexit -o pipefail -o noclobber -o nounset source="-" dest="-" -verbose=true +convert=false +verbose=2 +bitrate=192 help=false +temp="/tmp/converted" +convertart=false +coverartsize=200 + +CheckDeps() { + if [[ $1 == 0 ]]; then + # Check getopt + ! getopt --test > /dev/null + if [[ ${PIPESTATUS[0]} -ne 4 ]]; then + VerboseOutput 5 "\`getopt --test\` failed" + VerboseOutput 5 "Sorry, It seems that your shell is not supported" + VerboseOutput 5 "If you're using MacOS or another unix-like system, please install GNU getopt" + ExecTime + exit 1 + fi + fi + + if [[ $1 == 1 ]]; then + # Check lame + if [[ $convert == true && ! $(lame --version 2>/dev/null) ]]; then + VerboseOutput 5 "\`lame --version\` failed" + VerboseOutput 5 "Sorry, It seems that lame is not installed on your system" + VerboseOutput 5 "Please install lame from your repositories and make sure it is available in your \$PATH" + VerboseOutput 5 "Otherwise disable conversion" + ExecTime + exit 1 + fi + # Check EyeD3 + if [[ $convertart == true && ! $(eyeD3 --version 2>/dev/null) ]]; then + VerboseOutput 5 "\`eyeD3 --version\` failed" + VerboseOutput 5 "Sorry, It seems that eyeD3 is not installed on your system" + VerboseOutput 5 "Please install eyeD3 from your repositories and make sure it is available in your \$PATH" + VerboseOutput 5 "Otherwise disable albumart conversion" + ExecTime + exit 1 + fi + # Check ImageMagick + if [[ $convertart == true && ! $(convert --version 2>/dev/null) ]]; then + VerboseOutput 5 "\`convert --version\` failed" + VerboseOutput 5 "Sorry, It seems that ImageMagick is not installed on your system" + VerboseOutput 5 "Please install ImageMagick from your repositories and make sure it is available in your \$PATH" + VerboseOutput 5 "Otherwise disable albumart conversion" + ExecTime + exit 1 + fi + fi +} GetOptions() { # https://stackoverflow.com/a/29754866 - ! getopt --test > /dev/null - if [[ ${PIPESTATUS[0]} -ne 4 ]]; then - VerboseOutput "Fatal" "\`getopt --test\` failed" - echo "Sorry, It seems that your shell is not supported" - echo "If you're using mac or another unix-like system, please install GNU getopt" - exit 1 - fi - - # Parsing style without -s or -d - source=${*: -2:1} - dest=${*: -1:1} - - OPTIONS=s:d:vh - LONGOPTS=source:,dest:,verbose,help + OPTIONS=s:d:t:c::a::v::h + LONGOPTS=source:,dest:,temp:,convert::,resize-art::,verbose::,help # -use ! and PIPESTATUS to get exit code with errexit set # -temporarily store output to be able to check for errors @@ -40,12 +78,15 @@ GetOptions() { # read getopt’s output this way to handle the quoting right: eval set -- "$PARSED" - verbose=false # now enjoy the options in order and nicely split until we see -- while true; do case "$1" in -v|--verbose) - verbose=true + verbose=2 + if [[ $2 != "" ]]; then + verbose=${2} + shift + fi shift ;; -h|--help) @@ -60,18 +101,48 @@ GetOptions() { dest="$2" shift 2 ;; + -t|--temp) + temp="$2" + shift 2 + ;; + -c|--convert) + convert=true + bitrate=192 + if [[ $2 != "" ]]; then + bitrate=${2} + shift + fi + shift + ;; + -a|--resize-art) + convertart=true + coverartsize=200 + if [[ $2 != "" ]]; then + coverartsize=${2} + shift + fi + shift + ;; --) shift break ;; *) - echo "Programming error" + VerboseOutput 5 "Programming error" return 3 ;; esac done - if [[ $dest == "" ]] || [[ $source == "" ]]; then + if [[ ! -z "${1+x}" ]]; then + source="$1" + fi + + if [[ ! -z "${2+x}" ]]; then + dest="$2" + fi + + if [[ $dest == "-" ]] || [[ $source == "-" ]]; then help=true fi } @@ -85,81 +156,212 @@ Usage() { echo "Syncronises music from one folder to another." echo "" echo "Options:" - echo " -s, --source The source folder of the music" - echo " -d, --dest The destionation folder of the music" - echo " -v, --verbose Enable verbose output" - echo " -h, --help Display this help text" + echo " -s, --source The source folder of the music" + echo " -d, --dest The destination folder of the music" + echo " -t, --temp The temporary cache for converted files (default: /tmp/converted)" + echo " -c, --convert Convert files to a given bitrate in kbps before syncing (default: 192)" + echo " -a, --resize-art Resize album-art before syncing (default width: 200)" + echo " -v, --verbose <0-6> Set log level (default: 2)" + echo " -h, --help Display this help text" + echo "" + echo "Log levels:" + echo " 0 | Verbose" + echo " 1 | Debug" + echo " 2 | Info" + echo " 3 | Warning" + echo " 4 | Error" + echo " 5 | Fatal" + echo " 6 | Silence" + echo "" + echo "Exit Codes:" + echo " 1 Dependencies not met" + echo " 2 Invalid Argument" + echo " 3 Source Unreachable" + echo " 4 Destination Unreachable" + echo " 5 Command failed" echo "" } VerboseOutput() { - if [[ "$verbose" = true ]]; then - echo "[$1] $2" >&2 + level="" + if [[ $verbose -le $1 ]]; then + case "$1" in + 0) + level="\033[1;36mVerbose\033[0m" + ;; + 1) + level="\033[1;34m Debug \033[0m" + ;; + 2) + level="\033[1;37m Info \033[0m" + ;; + 3) + level="\033[1;33mWarning\033[0m" + ;; + 4) + level="\033[1;31m Error \033[0m" + ;; + + 5) + level="\033[1;30m Fatal \033[0m" + ;; + esac + echo -e "[$level] $2" >&2 fi } CreateFileList() { # ${1} /mnt/hdd/Example-Artist/Example-Album # ${2} /mnt/mtp/Example-Artist/Example-Album - # ${3} Example-Artist/Example-Album/ + # ${3} Example-Artist/Example-Album IFS="" - sourcepath="${1}/*" + sourcepath="${1/\[/\\\[}/*" + sourcepath="${sourcepath/\]/\\\]}" for file in $sourcepath; do - relfile="${file#"$1/"}" - if [[ -d "${1}/$relfile" ]]; then + origfile="${file#"$1/"}" + relfile=$(echo ${origfile} | sed -e 's/\(\.\)*$//g') + if [[ -d "${1}/$origfile" ]]; then newdir="${3}/$relfile" newdir=${newdir#"/"} - VerboseOutput "Info" "Entering $newdir" - CreateFileList "${1}/$relfile" "${2}/$relfile" "$newdir" - elif [[ ! -f "${2}/$relfile" || "${1}/$relfile" -nt "${2}/$relfile" ]]; then + VerboseOutput 1 "Entering $newdir" + CreateFileList "${1}/$origfile" "${2}/$relfile" "$newdir" + elif [[ "${1}/$origfile" != *".m3u" ]] && [[ ! -f "${2}/$relfile" || "${1}/$origfile" -nt "${2}/$relfile" ]]; then echo ${3}/$relfile >> /tmp/music-sync-filelist - VerboseOutput "Info" "Added: ${3}/${relfile}" + VerboseOutput 2 "Added: ${3}/${relfile}" fi done } ConvertFiles() { - mkdir -p /tmp/converted + curline=0 + percentage=0 while read -r line do - VerboseOutput "Info" "Converting $line" - if [[ "/tmp/converted/$line" = */* ]]; then - mkdir -p "/tmp/converted/${line%/*}"; + + if [[ ! -f "${source}/$line" ]]; then + VerboseOutput 5 "Source-file ${source}/$line Unreachable" + ExecTime + exit 3 + fi + + curline=$(expr ${curline} + 1) + total=$(cat /tmp/music-sync-filelist | wc -l) + percentage=$(echo "scale=4;${curline}/${total}" | bc) + percentage=$(echo "scale=2;${percentage}*100" | bc) + VerboseOutput 1 "Converting: $line" + VerboseOutput 2 "Progress: $curline / $total (${percentage%00}%) Step 1 of 2" + + if [[ "$temp/$line" = */* ]]; then + mkdir -p "$temp/${line%/*}"; fi; - lame -b 192 $source/$line /tmp/converted/$line 1>/dev/null 2>/dev/null - VerboseOutput "Info" "Converted $line" + if [[ ! -f "$temp/$line" || "${source}/$line" -nt "$temp/$line" ]]; then + lame -b ${bitrate} $source/$line $temp/$line 1>/dev/null 2>/dev/null + VerboseOutput 2 "Converted: $line" + if [[ $convertart == true ]]; then + mkdir -p "$temp/$line-images/" + eyeD3 --write-images "$temp/$line-images/" "$temp/$line" 1>/dev/null 2>/dev/null + convert "$temp/$line-images/FRONT_COVER.*" -resize ${coverartsize}x${coverartsize} "$temp/$line-images/FRONT_COVER.jpg" 1>/dev/null 2>/dev/null + eyeD3 --remove-all-images "$temp/$line" 1>/dev/null 2>/dev/null + eyeD3 --add-image "$temp/$line-images/FRONT_COVER.jpg:FRONT_COVER" "$temp/$line" 1>/dev/null 2>/dev/null + VerboseOutput 2 "Converted cover art: $line" + fi + + else + VerboseOutput 3 "$line already converted" + fi; + done < "/tmp/music-sync-filelist" } CopyFiles() { + curline=0 + percentage=0 while read -r line do - VerboseOutput "Info" "Copy $line" + if [[ ! -d "$dest" ]]; then + VerboseOutput 5 "Destination unreachable" + ExecTime + exit 4 + fi + + if [[ ! -f "${temp}/$line" ]]; then + VerboseOutput 5 "Source-file ${temp}/$line Unreachable" + ExecTime + exit 3 + fi + + curline=$(expr ${curline} + 1) + total=$(cat /tmp/music-sync-filelist | wc -l) + percentage=$(echo "scale=4;${curline}/${total}" | bc) + percentage=$(echo "scale=2;${percentage}*100" | bc) + + VerboseOutput 1 "Copying: $line" + VerboseOutput 2 "Progress: $curline / $total (${percentage%00}%) Step 2 of 2" + if [[ "$dest/$line" = */* ]]; then mkdir -p "$dest/${line%/*}"; fi; - cp -f $source/$line $dest/$line 1>/dev/null 2>/dev/null + + cp -f $temp/$line $dest/$line 1>/dev/null 2>/dev/null + + VerboseOutput 2 "Copied: $line" + done < "/tmp/music-sync-filelist" - VerboseOutput "Info" "Copied $line" } CleanUp() { - VerboseOutput "Info" "Cleaning Up" - rm "/tmp/music-sync-filelist" - rm -rf "/tmp/converted" - VerboseOutput "Info" "Done" + VerboseOutput 1 "Cleaning Up" + if [[ -f /tmp/music-sync-filelist ]]; then + rm "/tmp/music-sync-filelist" + fi + VerboseOutput 1 "Done" } +ExecTime() { + termin=$(date +"%s") + difftimelps=$(($termin-$begin)) + VerboseOutput 1 "$(($difftimelps / 60)) minutes and $(($difftimelps % 60)) seconds elapsed for Script Execution." +} + +ErrorHandler() { + VerboseOutput 5 "Error while executing $1" + CleanUp + exit 5 +} + +ExitHandler() { + VerboseOutput 5 "Aborted" + CleanUp + exit 0 +} + +trap 'ExitHandler' SIGINT +trap 'ErrorHandler $BASH_COMMAND' ERR + +CheckDeps 0 GetOptions $@ +CheckDeps 1 if [[ $help == true ]]; then Usage exit fi +if [[ -f /tmp/music-sync-filelist ]]; then + rm /tmp/music-sync-filelist +fi CreateFileList $source $dest "" if [[ ! -f /tmp/music-sync-filelist ]]; then - VerboseOutput "Info" "Nothing to do!" + VerboseOutput 2 "Nothing to do!" + CleanUp exit 0 fi -ConvertFiles +if [[ $convert == true ]]; then + ConvertFiles +else + if [[ $temp != "/tmp/converted" ]]; then + VerboseOutput 2 "Conversion not enabled. Ignoring cache folder" + fi + temp=$source +fi CopyFiles -CleanUp \ No newline at end of file +CleanUp +ExecTime