#!/bin/bash # #/ #/ Usage: #/ music-sync -s|--source -d|--dest #/ music-sync #/ #/ Syncronises music from one folder to another. #/ #/ Options: #/ -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) #/ -j, --jobs= Number of processes to use in multi-threading (default: nproc - 2) #/ -v, --verbose=<0-6> Set log level (default: 2) #/ -h, --help Display this help text #/ -p, --playlists Enable playlist sync #/ -m, --mapping=DIR1,DIR2 Add mappings for playlists from DIR1 to DIR2 #/ -w, --windows-format Use crlf and backslashes for playlists #/ #/ 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 #/ begin=$(date +"%s") # saner programming env: these switches turn some bugs into errors set -o errexit -o pipefail -o noclobber -o nounset source="-" dest="-" convert=false verbose=2 bitrate=192 help=false temp="/tmp/converted" convertart=false coverartsize=200 jobcount=$(expr $(nproc) - 2) multithread=false script_name=$(basename "${0}") script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) mappingdest='' mappingsrc='' windowspaths=false playlists=false CheckDeps() { if [[ $1 == 2 ]]; 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 VerboseOutput 0 "\`getopt --test\` succeeded" fi if [[ $1 == 1 ]]; then # Check ffmpeg if [[ $convert == true ]]; then if [[ ! $(ffmpeg -h 2>/dev/null) ]]; then VerboseOutput 5 "\`ffmpeg -h\` failed" VerboseOutput 5 "Sorry, It seems that ffmpeg is not installed on your system" VerboseOutput 5 "Please install ffmpeg from your repositories and make sure it is available in your \$PATH" VerboseOutput 5 "Otherwise disable conversion" ExecTime exit 1 fi VerboseOutput 0 "\`ffmpeg -h\` succeeded" fi # Check EyeD3 if [[ $convertart == true ]]; then if [[ ! $(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 VerboseOutput 0 "\`eyeD3 --version\` succeeded" fi # Check ImageMagick if [[ $convertart == true ]]; then if [[ ! $(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 VerboseOutput 0 "\`convert --version\` succeeded" fi # Check Gnu parallel if [[ $multithread == true ]]; then if [[ ! $(parallel -h 2>/dev/null) ]]; then VerboseOutput 5 "\`parallel -h\` failed" VerboseOutput 5 "Sorry, It seems that parallel is not installed on your system" VerboseOutput 5 "Please install gnu-parallel from your repositories and make sure it is available in your \$PATH" VerboseOutput 5 "Otherwise disable multithreading" ExecTime exit 1 fi VerboseOutput 0 "\`parallel -h\` succeeded" fi # Check playlist requirement if [[ $playlists == true ]]; then if [[ $windowspaths == true ]]; then if [[ ! $(unix2dos --version 2>/dev/null) ]]; then VerboseOutput 5 "\`unix2dos --version\` failed" VerboseOutput 5 "Sorry, It seems that unix2dos is not installed on your system" VerboseOutput 5 "Please install unix2dos from your repositories and make sure it is available in your \$PATH" VerboseOutput 5 "Otherwise disable windows-format playlists" ExecTime exit 1 fi fi VerboseOutput 0 "\`unix2dos --version\` succeeded" fi fi VerboseOutput 1 "Dependency test OK" } GetOptions() { # https://stackoverflow.com/a/29754866 OPTIONS=s:d:t:c::a::v::hj::pwm: LONGOPTS=source:,dest:,temp:,convert::,resize-art::,verbose::,help,jobs::,playlists,windows-format,mapping: # -use ! and PIPESTATUS to get exit code with errexit set # -temporarily store output to be able to check for errors # -activate quoting/enhanced mode (e.g. by writing out “--options”) # -pass arguments only via -- "$@" to separate them correctly ! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@") if [[ ${PIPESTATUS[0]} -ne 0 ]]; then # e.g. return value is 1 # then getopt has complained about wrong arguments to stdout Usage exit 2 fi # read getopt's output this way to handle the quoting right: eval set -- "$PARSED" # now enjoy the options in order and nicely split until we see -- while true; do case "$1" in -v|--verbose) if [[ $2 != "" ]]; then verbose=${2} fi shift 2 ;; -h|--help) help=true shift ;; -s|--source) VerboseOutput 0 "--source given." VerboseOutput 1 "Source is ${2}" source="$2" shift 2 ;; -d|--dest) VerboseOutput 0 "--dest given." VerboseOutput 1 "Destination is ${2}" dest="$2" shift 2 ;; -t|--temp) VerboseOutput 0 "--temp given" VerboseOutput 1 "Tempfolder is ${2}" temp="$2" shift 2 ;; -c|--convert) VerboseOutput 0 "--convert given" convert=true if [[ $2 != "" ]]; then bitrate=${2} fi VerboseOutput 1 "Converted bitrate is ${bitrate}" shift 2 ;; -a|--resize-art) VerboseOutput 0 "--resize-art given" convertart=true if [[ $2 != "" ]]; then coverartsize=${2} fi shift 2 VerboseOutput 1 "Album art will ${coverartsize}px wide" ;; -j|--jobs) VerboseOutput 0 "--jobs given" multithread=true if [[ $2 != "" ]]; then jobcount=${2} fi shift 2 VerboseOutput 1 "Multithreading will use ${jobcount} threads" ;; -p|--playlists) VerboseOutput 0 "--playlists given" playlists=true shift VerboseOutput 1 "Playlist sync enabled" ;; -w|--windows-format) VerboseOutput 0 "--windows given" windowspaths=true shift VerboseOutput 1 "Windows format playlists enabled" ;; -m|--mapping) VerboseOutput 0 "--mapping given" listmappingparam=(${2//,/ }) mappingsrc=${listmappingparam[0]:-""} mappingdest=${listmappingparam[1]:-""} shift 2 VerboseOutput 1 "Playlist mappings enabled source=${mappingsrc} dest=${mappingdest}" ;; --) shift break ;; *) VerboseOutput 5 "Programming error" return 3 ;; esac done if [[ ! -z "${1+x}" ]]; then if [[ ${source} != "-" ]]; then VerboseOutput 4 "Source provided twice. Continueing with ${1}" fi source="$1" VerboseOutput 1 "Source is ${1}" fi if [[ ! -z "${2+x}" ]]; then if [[ ${dest} != "-" ]]; then VerboseOutput 4 "Destination provided twice. Continueing with ${2}" fi dest="$2" VerboseOutput 1 "Destination is ${2}" fi if [[ $dest == "-" ]] || [[ $source == "-" ]]; then help=true fi VerboseOutput 1 "Checks OK. Going on" } Usage() { grep '^#/' "${script_dir}/${script_name}" | sed 's/^#\/\w*//' } VerboseOutput() { 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 IFS="" sourcepath="${1/\[/\\\[}/*" sourcepath="${sourcepath/\]/\\\]}" for file in $sourcepath; do origfile="${file#"$1/"}" relfile=$(echo ${origfile} | sed -e 's/\(\.\)*$//g' | tr -s ' ') mp3file=$(echo "${relfile}" | sed -e 's/.flac/.mp3/') VerboseOutput 0 "Checking ${3}${origfile}" if [[ -d "${1}/$origfile" ]]; then VerboseOutput 0 "${origfile} is folder" newdir="${3}$origfile" newdir=${newdir#"/"} VerboseOutput 1 "Entering $newdir" CreateFileList "${1}/$origfile/" "${2}/$relfile/" "$newdir/" elif [[ "${1}/$origfile" != *".m3u" ]] && [[ "${1}/$origfile" -nt "${2}/$mp3file" ]]; then echo ${3}$origfile >> /tmp/music-sync-filelist VerboseOutput 0 "${origfile} is newer in source" VerboseOutput 2 "Added: ${3}${origfile}" elif [[ $playlists == true ]] && [[ "${1}/$origfile" == *".m3u" ]] && [[ "${1}/$origfile" -nt "${2}/$mp3file" ]]; then echo ${3}$origfile >> /tmp/music-sync-filelist VerboseOutput 0 "${origfile} is newer in source" VerboseOutput 2 "Added playlist: ${3}${origfile}" fi done } Execute() { curline=0 percentage=0 IFS=" "; if [[ $multithread == true ]]; then VerboseOutput 3 "Running in parallel. Mind your load" export -f ProcessFile export -f ConvertFile export -f ConvertPlaylist export -f CopyFile export -f VerboseOutput export -f ExecTime export dest export source export bitrate export convert export temp export verbose export coverartsize export begin export windowspaths export mappingsrc export mappingdest parallel --jobs ${jobcount} --will-cite --line-buffer --arg-file "/tmp/music-sync-filelist" ProcessFile else for line in $(cat "/tmp/music-sync-filelist") do ProcessFile $line done fi VerboseOutput 2 "Done!" } ProcessFile() { line=${1} curline="$(grep -n "$line" /tmp/music-sync-filelist | head -n 1 | cut -d: -f1)" total=$(cat /tmp/music-sync-filelist | wc -l) percentage=$(echo "scale=4;${curline}/${total}" | bc) percentage=$(echo "scale=2;${percentage}*100" | bc) VerboseOutput 2 "Current File: $line" VerboseOutput 2 "Progress: $curline / $total (${percentage%00}%)" if [[ $convert == true ]]; then if [[ $line == *".m3u" ]]; then ConvertPlaylist "$line" else ConvertFile "$line" fi fi CopyFile "$line" } ConvertPlaylist() { line=${1} if [[ ! -f "${source}/$line" ]]; then VerboseOutput 5 "Source-file ${source}/$line Unreachable" ExecTime exit 3 fi if [[ ! -d $(dirname "$temp/$line") ]]; then VerboseOutput 0 "Creating folder $temp/${line%/*}" mkdir -p "$temp/${line%/*}"; fi; if [[ -f "$temp/$line" ]]; then rm "$temp/$line" fi VerboseOutput 0 "Creating playlist $temp/$line" IFS=" " for item in $(cat "$source/$line") do mp3item=$(echo $item | sed -e 's/\.*\//\//g'| tr -s ' ') if [[ $convert == true ]]; then mp3item=$(echo "$item" | sed -e 's/.flac/.mp3/') else mp3item=$(echo "$item") fi playline=$(echo "${mp3item}" | sed -e "s,$mappingsrc,$mappingdest,g") if [[ $windowspaths == true ]]; then playline=$(echo "${playline}" | sed -e 's,\/,\\,g') fi echo $playline >> $temp/$line done if [[ $windowspaths == true ]]; then VerboseOutput 0 "Converting lf to crlf" unix2dos $temp/$line 1>/dev/null 2>/dev/null fi VerboseOutput 1 "Converted: $line" } ConvertFile() { line=${1} if [[ ! -f "${source}/$line" ]]; then VerboseOutput 5 "Source-file ${source}/$line Unreachable" ExecTime exit 3 fi if [[ ! -d $(dirname "$temp/$line") ]]; then VerboseOutput 0 "Creating folder $temp/${line%/*}" mkdir -p "$temp/${line%/*}"; fi; mp3line=$(echo "$line" | sed -e 's/.flac/.mp3/') if [[ "${source}/$line" -nt "$temp/$mp3line" ]]; then if [[ $line != *".mp3" ]]; then VerboseOutput 3 "${line} will be converted to mp3-file ${mp3line}" fi VerboseOutput 0 "Converting MP3-file $line" ffmpeg -y -i "$source/$line" -acodec libmp3lame -map_metadata 0 -id3v2_version 3 -b:a ${bitrate}k "$temp/${mp3line}" 1>/dev/null 2>/dev/null if [[ $convertart == true ]]; then VerboseOutput 0 "Creating folder $temp/${mp3line}-images/" mkdir -p "$temp/${mp3line}-images/" VerboseOutput 0 "Extracted albumart" eyeD3 --write-images "$temp/${mp3line}-images/" "$temp/${mp3line}" 1>/dev/null 2>/dev/null frontcovers=(${temp}/${mp3line}-images/FRONT_COVER.*) if [ -e "$frontcovers" ]; then VerboseOutput 0 "Converting albumart" convert "$temp/${mp3line}-images/FRONT_COVER.*" -resize ${coverartsize}x${coverartsize} "$temp/${mp3line}-images/FRONT_COVER.jpg" 1>/dev/null 2>/dev/null eyeD3 --remove-all-images "$temp/${mp3line}" 1>/dev/null 2>/dev/null VerboseOutput 0 "Embedding albumart" eyeD3 --add-image "$temp/${mp3line}-images/FRONT_COVER.jpg:FRONT_COVER" "$temp/${mp3line}" 1>/dev/null 2>/dev/null VerboseOutput 1 "Converted cover art: ${mp3line}" else VerboseOutput 4 "No front cover art found for ${mp3line}" fi fi VerboseOutput 1 "Converted: $line" else VerboseOutput 3 "$line already converted" fi; } CopyFile() { line=${1} if [[ ! -d "$dest" ]]; then VerboseOutput 5 "Destination unreachable" ExecTime exit 4 fi if [[ $convert == true ]]; then mp3line=$(echo "$line" | sed -e 's/.flac/.mp3/') else mp3line=$(echo "$line") fi if [[ ! -f "${temp}/$mp3line" ]]; then VerboseOutput 5 "Source-file ${temp}/$mp3line Unreachable" ExecTime exit 3 fi destline=$(echo $mp3line | sed -e 's/\.*\//\//g'| tr -s ' ') VerboseOutput 1 "Copying: $line" if [[ ! -d $(dirname "$dest/$line") ]]; then VerboseOutput 0 "Creating folder $dest/${line%/*}" mkdir -p "$dest/${destline%/*}"; fi; cp -fv "$temp/$mp3line" "$dest/${destline}" 1>/dev/null 2>/dev/null VerboseOutput 1 "Copied: $line" } CleanUp() { VerboseOutput 1 "Cleaning Up" if [[ -f /tmp/music-sync-filelist ]]; then VerboseOutput 1 "Removing filelist" 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 2 GetOptions $@ CheckDeps 1 if [[ $help == true ]]; then Usage exit fi if [[ -f /tmp/music-sync-filelist ]]; then rm /tmp/music-sync-filelist fi VerboseOutput 2 "Scanning for new or updated files" CreateFileList $source $dest "" if [[ ! -f /tmp/music-sync-filelist ]]; then VerboseOutput 2 "Nothing to do!" CleanUp exit 0 fi if [[ $convert == true ]]; then temp=${temp}/bitrate-${bitrate} if [[ $convertart == true ]]; then temp="${temp}/coverart-${coverartsize}" else temp="${temp}/coverart-original" fi VerboseOutput 2 "Conversion enabled. Using $temp as temp-folder" else if [[ $temp != "/tmp/converted" ]]; then VerboseOutput 2 "Conversion not enabled. Ignoring temp folder" fi temp=$source fi Execute CleanUp ExecTime