Sync Script for Linux devices
#1
Hey y'all! I made a bash sync script for the Tangara!

What it assumes:
 * you are running linux
 * You have a single source folder containing all your music files
 * You want that single library folder structure to be mirrored on your tangara
 * You have ffmpeg installed and accessible on your PATH
 * you have read access to your music library
 * You too struggled to get https://github.com/complexlogic/EasyAudioSync running on your system

What it does:
 * Converts incompatible aac/alac files
 * Handles embedded and folder-local cover files
 * lets you choose your target audio settings
 * Lets you choose if you want to compress lossless audio to save space
 * lets you choose which categories of lossy files you want converted

What it doesn't do:
* Anything other than music. Playlists don't work either.
 * Delete things. 
 * It doesn't update your tangara if you remove an album from your library
 * It does its best to never overwrite any existing files (But no promises! Backups are YOUR job!)

Running the script:

Step 1:

Check for ffmpeg in your path:
Code:
ffmpeg -version
[*]
Step 2:
Copy your music library to a folder the script has read permission in!

Step 3:
Make a new folder somewhere the script has write permissions in.

Step 4:
Paste this code into an empty shell script file.

Code:
#!/bin/bash

#First two variables are the only ones you are required to adjust
#recommend using a copy of your library and sending to another folder (and not the tangara card)
#then just do rsync --progress source dest
#IF YOU LOSE DATA IT'S YOUR OWN FAULT FOR NOT BACKING THINGS UP
#that said, this is deigned to allow for reruns to sync any new music files it finds without overwriting
#any existing data at target directory

#source and destination folders
library="/home/dageek247/Music Copy"
tangara="/home/dageek247/tangaraMusic"

#Options are copy or convert
  #copy keeps lossless files as is
  #convert converts all lossless files to target settings to save space
reincodelossless=copy

#choose to reincode lossy audio; turn on for uniformity.
#aac and alac will be reencoded regardless of this setting.
reincodelossy=false
#choose what to do with unknown audio formats
#unkown audio is all audio files that ffmpeg can read, but is not listed at
#https://cooltech.zone/tangara/docs/music-library/#supported-audio-formats
reincodeunknowncodecs=true

#this script can handle album art for the tangara
#it grabs whatever cover art it can find and then downsizes it to fit the tangara screen
#capable of using embedded cover art, no problem.
grabalbumart=true

#format to convert to whenever conversion is required
#this will be run on any aac and alac audio
targetcodec=libopus
targetextension=ogg
#use kbps for this option
targetquality=256k


#main script loop, iterate through every file in source library and run commands depnding on prior settings
#IFS command keeps the find command as one string instead of splitting strings delimitted by spaces
IFS=$'\n'

#run file operations in temp directory
mkdir temp
cd temp

#loop through all the files in the source library
for file in $(find $library); do
    filename=$(basename -a $file)
    cutfilename=${filename%.*}
    filestruct="${file#$library}"
    cutfilestruct=${filestruct%.*}
    cutfolderstruct=${filestruct%/*}
    #echo "Current Working File: $filestruct"

    #ensure folder structure matches source library
    if [ -d $file ] ; then #if file is folder
        if [ -d "$tangara$filestruct" ] ; then #if folder exists in target library
            :
        else #if folder does not exist in target library
            #create folder in target directory
            mkdir -p "$tangara$filestruct"
        fi
    elif [ -f $file ] ; then
        #file is file
       
        #only do very slow ffprobe task if the possible target files don't exist
        if [ -f $tangara$cutfilestruct.$targetextension ] ; then
            echo "$cutfilestruct.$targetextension already exists on target directory! Skipping."
            continue
        elif [ -f $tangara$filestruct ] ; then
            echo "$filestruct already exists on target directory! Skipping."
            continue
        else
            ffproberesults=$(ffprobe $file 2>&1)
            #copy the current working file to the current directory
            cp -f $library$filestruct $filename
        fi
               
        #check codec of file
        filecodec=na
        filecodectype=na
     

        #obtain current working file codec
        if [[ $(echo $ffproberesults | grep -w "Audio: aac") ]] ; then
            filecodec=aac
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: alac") ]] ; then
            filecodec=alac
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: flac") ]] ; then
            filecodec=flac
            filecodectype=lossless
        elif [[ $(echo $ffproberesults | grep -w "Audio: wav") ]] ; then
            filecodec=wav
            filecodectype=lossless
        elif [[ $(echo $ffproberesults | grep -w "Audio: mp3") ]] ; then
            filecodec=mp3
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: vorbus") ]] ; then
            filecodec=vorbus
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: opus") ]] ; then
            filecodec=opus
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: ") ]] ; then
            filecodec=unk
            filecodectype=lossy
        fi
       
        #handle cover.jpg
        #check if we want to send a cover.jpg to the target library
        if [ $grabalbumart = true ] ; then
            if [ -f $tangara$cutfolderstruct/cover.jpg ] ; then
                #do nothing because we already have a cover.jpg at the target location
                :
            else
                #Check if this file is a cover.jpg equivalent and sync it to the tangara.
                if [[ $cutfilename == "cover" || $cutfilename == "folder" ]] ; then
                    #attempt to compress it to fit the tangara screen size while keeping source ratio
                    ffmpeg -y -hide_banner -loglevel error -i $filename -qscale:v 2 -vf "scale='if(gt(iw,ih),160,-1)':'if(gt(iw,ih),-1,128)" cover.jpg
                    mv -u cover.jpg $tangara$cutfolderstruct/cover.jpg
                #ensure file is some sort of music file before running this
                elif [[ $filecodectype == "lossless" || $filecodectype == "lossy" ]] ; then
                    #check for embedded art prior to converting
                    if [[ $(echo $ffproberesults | grep -w "Video: ") ]] ; then
                        ffmpeg -y -hide_banner -loglevel error -i $filename -qscale:v 2 -vf "scale='if(gt(iw,ih),160,-1)':'if(gt(iw,ih),-1,128)" cover.jpg
                        mv -u cover.jpg $tangara$cutfolderstruct/cover.jpg
                    fi
                fi
            fi
        fi
       
        #choose what to do with the file
        if [[ $filecodectype == "lossless" ]] ; then
            #First check to see if an encoded or copy version already exists on target library
            if [ -f $tangara$cutfilestruct.$targetextension ] ; then
                echo "$cutfilestruct.$targetextension already exists on target directory! Skipping."
            elif [ -f $tangara$filestruct ] ; then
                echo "$filestruct already exists on target directory! Skipping."
            else
                if [[ $reincodelossless == "limit" ]] ; then
                    echo "we can't do the limit option just yet"
                elif [[ $reincodelossless == "copy" ]] ; then
                    #safely copy the file to the tangara device without overwriting existing files
                    echo "Copying lossless audio file $filename over to $tangara$filestruct"
                    mv -u $filename $tangara$filestruct
                elif [[ $reincodelossless == "convert" ]] ; then
                    echo "Converting $filename to $cutfilestruct.$targetextension"
                    ffmpeg -n -hide_banner -loglevel error -i $filename -map 0:a -c:a $targetcodec -b:a $targetquality $cutfilename.$targetextension
                    echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                    mv -u $cutfilename.$targetextension $tangara$cutfilestruct.$targetextension
                fi
            fi
        elif [[ $filecodectype == "lossy" ]] ; then
            #start with unspported codecs which will always be converted
            if [[ $filecodec == "aac" || $filecodec == "alac" ]] ; then
                if [ -f $tangara$cutfilestruct.$targetextension ] ; then
                    echo "$tangara$cutfilestruct.$targetextension already exists. Skipping."
                else
                    echo "Converting $filename to $cutfilestruct.$targetextension"
                    ffmpeg -n -hide_banner -loglevel error -i $filename -map 0:a -c:a $targetcodec -b:a $targetquality $cutfilename.$targetextension
                    echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                    mv -u $cutfilename.$targetextension $tangara$cutfilestruct.$targetextension
                fi
            #move to supported lossy codecs
            elif [[ $filecodec == "mp3" || $filecodec == "opus" || $filecodec == "vorbis" ]] ; then
                #echo "$filename is a lossy file. Convert Target result is $tangara$filestruct"
                if [ $reincodelossy = false ] ; then
                    #verify if a copied file version exists
                    if [ -f $tangara$filestruct ] ; then
                        echo "$filestruct already exists. Skipping."
                    else
                        #safely copy the lossy file over to the tangara device.
                        echo "Copying lossy audio file $filename over to $tangara$filestruct"
                        mv -u $filename $tangara$filestruct
                    fi
                elif [ $reincodelossy = true ] ; then
                    #verify if a converted file version exists
                    if [ -f $tangara$cutfilestruct.$targetextension ] ; then
                        echo "$cutfilestruct.$targetextension already exists. Skipping."
                    else
                        echo "Converting $filename to $cutfilestruct.$targetextension"
                        ffmpeg -n -hide_banner -loglevel error -i $filename -map 0:a -c:a $targetcodec -b:a $targetquality $cutfilename.$targetextension
                        echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                        mv -u $cutfilename.$targetextension $tangara$cutfilestruct.$targetextension
                    fi
                fi
            #handle unknown but still audio codecs
            elif [[ $filecodec == "unk" ]] ; then
                if [ reincodeunknowncodecs = true ] ; then
                    #verify if a converted file version exists
                    if [ -f $tangara$cutfilestruct.$targetextension ] ; then
                        echo "$cutfilestruct.$targetextension already exists. Skipping."
                    else
                        echo "Converting $filename to $cutfilestruct.$targetextension"
                        ffmpeg -n -hide_banner -loglevel error -i $filename -map 0:a -c:a $targetcodec -b:a $targetquality $cutfilename.$targetextension
                        echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                        mv -u $cutfilename.$targetextension $tangara$cutfilestruct.$targetextension
                    fi
                else
                    #safely copy the unknown audio file over to the tangara device.
                    mv -u $filename $tangara$filestruct
                fi
            fi
        fi
    fi
done

#temp directory cleanup
cd ..
rm -rf temp

Step 5:
Edit the first two variables to match the locations of your duplicate music library and your new writable folder.

Step 6:
Run the script and watch it go!

Step 7:
Rsync the destination folder contents to your tangara sd card.
Code:
rsync --progress "/home/dageek247/tangaraMusic" "/media/SDCard01/Music"

Step 8:
Rerun the script whenever you have new music to add!


I consider this to be version 1.0. Its feature complete (for now) but bug testing is only mildly been done!
  Reply
#2
Star 
hey, nice job!
I've got three issues in my mind after a quick look - two aren't that big of a deal and one might cause unexpected results
1. less serious one is creating temp dir in current working directory - it will fail if temp dir exists on mkdir temp (but won't fail the whole script). mktemp -d may be more suitable in here
2. you may want to take a look at bash(1) manpage, specifically at shell builtin commands section set command (-e, -u and -o pipefail options)
3. the more serious one - filenames should be quoted, as they may contain spaces and this will cause weird behavior

other than this, it looks good!
  Reply
#3
I'm just leaving everything as flac right now but wanted to share my invocation for rclone, it's been a bit faster/cleaner for me than rsync (probably I just don't know rsync well enough but when you have a hammer everything is a nail)! Obviously very specific to my particular music library but an example of an rclone invocation at least.

Code:
#!/usr/bin/env bash

set -euo pipefail

rclone \
  sync \
  --ignore-case \
  --exclude \*.jpg \
  --exclude \*.webp \
  --exclude \*.mkv \
  --exclude \*.mp4 \
  --exclude \*.aac \
  --exclude \*.m4a \
  --no-update-modtime \
  --progress \
  /share/plex-media/music/ \
  /share/external/DEV3303_1/Music/
  Reply
#4
(2025-04-22, 09:36 PM)tebriel Wrote: I'm just leaving everything as flac right now but wanted to share my invocation for rclone, it's been a bit faster/cleaner for me than rsync (probably I just don't know rsync well enough but when you have a hammer everything is a nail)! Obviously very specific to my particular music library but an example of an rclone invocation at least.

Code:
#!/usr/bin/env bash

set -euo pipefail

rclone \
  sync \
  --ignore-case \
  --exclude \*.jpg \
  --exclude \*.webp \
  --exclude \*.mkv \
  --exclude \*.mp4 \
  --exclude \*.aac \
  --exclude \*.m4a \
  --no-update-modtime \
  --progress \
  /share/plex-media/music/ \
  /share/external/DEV3303_1/Music/


The script I shared actually only copies over files with audio to the target directory (and adds the album art, if selected). Everything else gets ignored by it. I haven't tested this, but i'm pretty sure it would grab  just the audio from a movie file if the movie had audio which needed to be converted, and would paste the whole movie file to the target directory if the movie audio didn't 'need' conversion.
  Reply
#5
I'm coming at y'all with a hot new update! I've taken pgronkievitz's advice, and also made a fix!

Here's what changed:
  • Code Improvements:
  • removed extra file detection if statements
  • changed to using mktemp
  • put filenames in quotes
  • Bugfixes:
  • fixed wav files not being detected by the script!
  • New Features:
  • Added a limit option for lossless files!

Turns out I was using the wrong codec when looking at wav files in ffprobe, so they would just get silently ignored and i would be none the wiser if I looked at the folder structure. I've also added a little message to tell you if a file has been ignored like that again. 

The other thing I have done is add a 'limit' option for lossless files! If you change to that new variable in the script, it will convert any lossless files which have a bitrate higher than the specified number! I've found this useful for flacs that have over 4mbps data rate. My bluetooth speaker (and / or the tangara itself) tends to stutter really badly whenever it tries to play a file with over 4mbps of data.

Finally, here's the new script!

Code:
#!/bin/bash

#First two variables are the only ones you are required to adjust
#recommend using a copy of your library and sending to another folder (and not the tangara card)
#then just do rsync --progress source dest
#IF YOU LOSE DATA IT'S YOUR OWN FAULT FOR NOT BACKING THINGS UP
#that said, this is deigned to allow for reruns to sync any new music files it finds without overwriting
#any existing data at target directory

#source and destination folders
library="/media/MusicDrive/musiclibrary"
tangara="/media/TangaraSD/Music"

#Options are copy, convert, or limit
  #copy keeps lossless files as is
  #convert converts all lossless files to target settings to save space
  #limit converts all lossless files which have a higher average bitrate than $maxallowedbitrate
  #i've found that my bluetooth setup tends to stutter above 4mbps
reincodelossless=limit
maxallowedbitrate=2048

#choose to reincode lossy audio; turn on for uniformity.
#aac and alac will be reencoded regardless of this setting.
reincodelossy=false
#choose what to do with unknown audio formats
#unkown audio is all audio files that ffmpeg can read, but is not listed at
#https://cooltech.zone/tangara/docs/music-library/#supported-audio-formats
reincodeunknowncodecs=true


#this script can handle album art for the tangara
#it grabs whatever cover art it can find and then downsizes it to fit the tangara screen
#capable of using embedded cover art, no problem.
grabalbumart=true

#format to convert to whenever conversion is required
#this will be run on any aac and alac audio
targetcodec=libopus
targetextension=ogg
#use kbps for this option
#opus is indistinguishable from lossless for the average listener at 190kbps.
#https://www.reddit.com/r/ffmpeg/comments/oa9f0x/comment/iifo6nu/
targetquality=192k


#main script loop, iterate through every file in source library and run commands depnding on prior settings
#IFS command keeps the find command as one string instead of splitting strings delimitted by spaces
IFS=$'\n'

#run file operations in temp directory
workingDirectory=$(mktemp -d temp.XXX)
cd $workingDirectory

#loop through all the files in the source library
for file in $(find $library | sort); do
    filename=$(basename -a $file)
    cutfilename=${filename%.*}
    filestruct="${file#$library}"
    cutfilestruct=${filestruct%.*}
    cutfolderstruct=${filestruct%/*}
    #echo "Current Working File: $filestruct"

    #ensure folder structure matches source library
    if [ -d $file ] ; then #if file is folder
        if [ -d "$tangara$filestruct" ] ; then #if folder exists in target library
            :
        else #if folder does not exist in target library
            #create folder in target directory
            mkdir -p "$tangara$filestruct"
        fi
    elif [ -f $file ] ; then
        #file is file
       
        #only do very slow ffprobe task if the possible target files don't exist
        if [ -f $tangara$cutfilestruct.$targetextension ] ; then
            echo "$cutfilestruct.$targetextension already exists on target directory! Skipping."
            continue
        elif [ -f $tangara$filestruct ] ; then
            echo "$filestruct already exists on target directory! Skipping."
            continue
        else
            ffproberesults=$(ffprobe "$file" 2>&1)
            #copy the current working file to the current directory
            cp -f $library$filestruct $filename
        fi
               
        #check codec of file
        filecodec=na
        filecodectype=na
        #used to check for too much data for the tangara to handle, not quality
        filebitrate=na
      

        #obtain current working file codec
        if [[ $(echo $ffproberesults | grep -w "Audio: aac") ]] ; then
            filecodec=aac
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: alac") ]] ; then
            filecodec=alac
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: flac") ]] ; then
            filecodec=flac
            filecodectype=lossless
        elif [[ $(echo $ffproberesults | grep "Audio: pcm") ]] ; then
            filecodec=wav
            filecodectype=lossless
        elif [[ $(echo $ffproberesults | grep -w "Audio: mp3") ]] ; then
            filecodec=mp3
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: vorbus") ]] ; then
            filecodec=vorbus
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: opus") ]] ; then
            filecodec=opus
            filecodectype=lossy
        elif [[ $(echo $ffproberesults | grep -w "Audio: ") ]] ; then
            filecodec=unk
            filecodectype=lossy
        fi
       
        #obtain current bitrate of audio file
        tempbitrate=$(echo $ffproberesults | sed -n -e 's/^.*bitrate: //p')
        filebitrate=$(echo $tempbitrate | cut -d ' ' -f 1)
        #echo "The bitrate of this audio file is $filebitrate kbps."
       
        #handle cover.jpg
        #check if we want to send a cover.jpg to the target library
        if [ $grabalbumart = true ] ; then
            if [ -f $tangara$cutfolderstruct/cover.jpg ] ; then
                #do nothing because we already have a cover.jpg at the target location
                :
            else
                #Check if this file is a cover.jpg equivalent and sync it to the tangara.
                if [[ $cutfilename == "cover" || $cutfilename == "folder" ]] ; then
                    #attempt to compress it to fit the tangara screen size while keeping source ratio
                    ffmpeg -y -hide_banner -loglevel error -i "$filename" -qscale:v 2 -vf "scale='if(gt(iw,ih),160,-1)':'if(gt(iw,ih),-1,128)" cover.jpg
                    mv -u cover.jpg "$tangara$cutfolderstruct/cover.jpg"
                #ensure file is some sort of music file before running this
                elif [[ $filecodectype == "lossless" || $filecodectype == "lossy" ]] ; then
                    #check for embedded art prior to converting
                    if [[ $(echo $ffproberesults | grep -w "Video: ") ]] ; then
                        ffmpeg -y -hide_banner -loglevel error -i "$filename" -qscale:v 2 -vf "scale='if(gt(iw,ih),160,-1)':'if(gt(iw,ih),-1,128)" cover.jpg
                        mv -u "cover.jpg $tangara$cutfolderstruct/cover.jpg"
                    fi
                else
                    if [[ $filecodec == "na" ]] ; then
                        #not a cover or audio file. Tell user it was skipped and let them deal with it.
                        echo "$filename is an unknown type of file. No actions were taken for this file."
                        continue
                    fi
                fi
            fi
        fi
       
        #choose what to do with the file
        if [[ $filecodectype == "lossless" ]] ; then
            if [[ $reincodelossless == "limit" ]] ; then
                #echo "we can't do the limit option just yet"
                if (( $filebitrate > $maxallowedbitrate )) ; then
                    echo "Converting lossless audio file $filename to $cutfilestruct.$targetextension because it has a bitrate of $filebitrate kbps."
                    ffmpeg -n -hide_banner -loglevel error -i "$filename" -map 0:a -c:a $targetcodec -b:a $targetquality "$cutfilename.$targetextension"
                    #echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                    mv -u "$cutfilename.$targetextension" "$tangara$cutfilestruct.$targetextension"
                    #remove the unconverted file from the temp directory
                    rm "$filename"
                else
                    echo "Copying lossless audio file $filename over to $tangara$filestruct"
                    mv -u "$filename" "$tangara$filestruct"
                fi
            elif [[ $reincodelossless == "copy" ]] ; then
                #safely copy the file to the tangara device without overwriting existing files
                echo "Copying lossless audio file $filename over to $tangara$filestruct"
                mv -u "$filename" "$tangara$filestruct"
            elif [[ $reincodelossless == "convert" ]] ; then
                echo "Converting $filename to $cutfilestruct.$targetextension"
                ffmpeg -n -hide_banner -loglevel error -i "$filename" -map 0:a -c:a $targetcodec -b:a $targetquality "$cutfilename.$targetextension"
                #echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                mv -u "$cutfilename.$targetextension" "$tangara$cutfilestruct.$targetextension"
                #remove the unconverted file from the temp directory
                rm "$filename"
            fi
        elif [[ $filecodectype == "lossy" ]] ; then
            #start with unspported codecs which will always be converted
            if [[ $filecodec == "aac" || $filecodec == "alac" ]] ; then
                echo "Converting $filename to $cutfilestruct.$targetextension"
                ffmpeg -n -hide_banner -loglevel error -i "$filename" -map 0:a -c:a $targetcodec -b:a $targetquality "$cutfilename.$targetextension"
                #echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                mv -u "$cutfilename.$targetextension" "$tangara$cutfilestruct.$targetextension"
                #remove the unconverted file from the temp directory
                rm "$filename"
            #move to supported lossy codecs
            elif [[ $filecodec == "mp3" || $filecodec == "opus" || $filecodec == "vorbis" ]] ; then
                #echo "$filename is a lossy file. Convert Target result is $tangara$filestruct"
                if [ $reincodelossy = false ] ; then
                    echo "Copying lossy audio file $filename over to $tangara$filestruct"
                    mv -u "$filename" "$tangara$filestruct"
                elif [ $reincodelossy = true ] ; then
                    echo "Converting $filename to $cutfilestruct.$targetextension"
                    ffmpeg -n -hide_banner -loglevel error -i "$filename" -map 0:a -c:a $targetcodec -b:a $targetquality "$cutfilename.$targetextension"
                    #echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                    mv -u "$cutfilename.$targetextension" "$tangara$cutfilestruct.$targetextension"
                    #remove the unconverted file from the temp directory
                    rm "$filename"
                fi
            #handle unknown but still audio codecs
            elif [[ $filecodec == "unk" ]] ; then
                if [ reincodeunknowncodecs = true ] ; then
                    echo "Converting $filename to $cutfilestruct.$targetextension"
                    ffmpeg -n -hide_banner -loglevel error -i "$filename" -map 0:a -c:a $targetcodec -b:a $targetquality "$cutfilename.$targetextension"
                    #echo "Copying $cutfilestruct.$targetextension over to $tangara$cutfilestruct"
                    mv -u "$cutfilename.$targetextension" "$tangara$cutfilestruct.$targetextension"
                    #remove the unconverted file from the temp directory
                    rm "$filename"
                else
                    #safely copy the unknown audio file over to the tangara device and hope the unknoiwn codec works.
                    mv -u "$filename" "$tangara$filestruct"
                fi
            fi
        fi
    fi
done

#temp directory cleanup
cd ..
rm -rf $workingDirectory
  Reply


Forum Jump: