Sync Script for Linux devices - dageek247 - 2025-04-08
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:
[*]
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!
RE: Sync Script for Linux devices - pgronkievitz - 2025-04-08
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!
RE: Sync Script for Linux devices - tebriel - 2025-04-22
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/
RE: Sync Script for Linux devices - dageek247 - 2025-04-28
(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.
RE: Sync Script for Linux devices - dageek247 - 2025-05-03
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
|