123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- #!/bin/bash
- #
- #/
- #/ Usage:
- #/ music-sync <options> -s|--source <source> -d|--dest <destination>
- #/ music-sync <options> <source> <destination>
- #/
- #/ Syncronises music from one folder to another.
- #/
- #/ Options:
- #/ -s, --source=<source> The source folder of the music
- #/ -d, --dest=<destination> The destination folder of the music
- #/ -t, --temp=<folder> The temporary cache for converted files (default: /tmp/converted)
- #/ -c, --convert=<bitrate> Convert files to a given bitrate in kbps before syncing (default: 192)
- #/ -a, --resize-art=<width> Resize album-art before syncing (default width: 200)
- #/ -j, --jobs=<nproc> 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
|