ChatGPT & Shell Scripting

Posted on Jan 20, 2024
tl;dr: Leveraging ChatAI for quick and easy problem solving

ChatAI - make me a shell script

One of the things I’ve always ended up doing is writing little shell scripts, or one liners, to do stuff, generally this requirement comes from some sort of needing to tickle of my rather extensive (1.4Tb currently) music library.

I recently got a new music player (A Zishan - now Zisan - Z5). It’s a pretty awesome DAP and does a lot more than the previous generation (the Zishan DSD with an AK9945 DAC), including much improved displaying of Album Art.

One of the shell scripts I have is one I use to sync certain files now and again, to copy my new music to whatever SDcard or USB drive I’m wanting to put stuff on. This is pretty simple, all it does is find anything created in the last N days (45) and uses rsync to copy it from my host where the music is. This will be run on a Mac, so we use gnu-sed (brew install gsed):

#!/bin/sh
eval `ssh-agent`
ssh-add
ssh nas "find /export/music/FLAC/ -type d -ctime -45 -mindepth 3" > targets.txt
gsed -i 's/$/\//' targets.txt
gsed -i 's/\/export\/music\/FLAC\///' targets.txt
rsync -avvp --progress --stats --inplace  --files-from=./targets.txt nas:/export/music/FLAC/ .

My music is managed by MusicBrainz Picard (which is absolutely incredible). This organises my music into directories that look like this kind of structure:

./Archive Albums/Electronic/Oliver Koletzki - 2009 - Großstadtmärchen
./Archive Albums/Indie/Matt Harding - 2006 - Expectation
./Archive Albums/Reggae/The Meditations - 1994 - Deeper Roots~ The Best of the Meditations
./Archive Compilations/Electronic/Various Artists - 2006 - Zentertainment 2006

This is great, and means if I browse the filesystem for genres I can find them. There’s a world of other supporting infrastructure I have to support my music, e.g. owntone, shairport-sync, and about a million raspberry pi’s.

I do however occasionally need to give my library a tickle, for example one of the challenges was that I download correspoinding lyrics (lrc) files as well as my music files, but upon them being renamed by Picard, the lyrics filename no longer matches the music track name. I don’t really care, but my daughter likes to read the lyrics along the track being played on her music player. A low priority task for me, so one day I figured I’d see if ChatGPT could do the heavy lifting for me. We’ll come back onto that later, for now a simpler request:

I want you to write a bash script which will run on freebsd, to recurse directories and 
for each of the directories containing music files, check if there is a cover.jpg file present. 
If there isn't a cover.jpg file, then check the directory for any other picture files.   
The script should be quiet unless if finds a directory without any albumart

Broadly, it did the right thing - it needed to be told that my music was also in m4a format (it guessed I only had flac and mp3 and ogg files), and that on FreeBSD bash is in /usr/local/bin/bash, but it took me all of 30 seconds to come up with a script that does what I wanted. Here’s the script.

#!/usr/local/bin/bash

# Function to check for music files in a directory
contains_music_files() {
    local dir="$1"
    shopt -s nullglob
    local files=("${dir}"/*.mp3 "${dir}"/*.wav "${dir}"/*.flac "${dir}"/*.m4a)
    shopt -u nullglob
    [[ ${#files[@]} -gt 0 ]]
}

# Function to check for cover.jpg in a directory
contains_cover_jpg() {
    local dir="$1"
    [[ -f "${dir}/cover.jpg" ]]
}

# Function to check for any image files in a directory
contains_image_files() {
    local dir="$1"
    shopt -s nullglob
    local files=("${dir}"/*.jpg "${dir}"/*.png "${dir}"/*.gif)
    shopt -u nullglob
    [[ ${#files[@]} -gt 0 ]]
}

# Main function to recurse directories
find_directories_without_albumart() {
    local dir="$1"
    for subdir in "$dir"/*; do
        if [[ -d "$subdir" ]]; then
            if contains_music_files "$subdir"; then
                if ! contains_cover_jpg "$subdir" && ! contains_image_files "$subdir"; then
                    echo "No album art in $subdir"
                fi
            fi
            find_directories_without_albumart "$subdir"
        fi
    done
}

# Start the script from the specified directory or the current directory
start_dir=${1:-.}
find_directories_without_albumart "$start_dir"

Nifty.

Last week, I asked it to write this next script for me. This was more of a back and forth affair, i.e. give it one simple thing to do, then ask it to expand on that. I’m finding the cleaner you are in your requirements, the more accurate the output - no surprises there.

challenge for you - on a linux system, find all the files which end in .lrc, 
for example this : Archive Albums/Electronic/Shamir - 2023 - Homo Anxietatem/11 - 
Shamir - The Devil Said the Blues is All I'll Know.lrc
and then find the corresponding m4a file, which is named like this: 
Archive Albums/Electronic/Shamir - 2023 - Homo Anxietatem/11 - The Devil Said the Blues Is All I'll Know.m4a
you are to rename the .lrc file so it matches the m4a file, but with a .lrc extenstion.  
The track number is the first 2 digits of the file name, and you should match on that.  
You have the usual unix utilities available (find, sed, grep, awk, etc).  Write this script in bash

Almost immediately I had a script that worked - but it didn’t recurse into directories. I never gave it this requirement, so stating this it created an updated version. Onto the next minor task I had on my TODO list…

Occasionally I have some duplicates, so I asked it to incorporate those deletions into the script:

User
I have another problem - sometimes I have multiple copies of the same file, with a (1) in 
the second file.  Examples are "Is This Real~ (Can You Hear Yourself~).m4a" and "Is This Real~ 
(Can You Hear Yourself~) (1).m4a" .  I would like you to amend the script to also check for 
these duplicate files, and delete the copy with (1) in the name

Happily it amended the script to delete anything with (1) in the filename. Caveat Emptor - you need to read and examine everything it emits. No biggie, just ask to rework the script with this requirement.

User
I want the deletion script to check to see if there is a similar named file in the directory, 
which is the same but without (1) in the name

Sorted.

The last task on the TODO list I needed to do was to fix some missing album art - might as well do that at the same time:

the third task - for each of the m4a files, check to see if there is album art 
in the media file.  If not, check for a file named cover.jpg in the directory 
we are in, and attach it.  AtomicParsley is installed for you to do this

Off it went, with only a few minor tweaks, such as telling it I want the directory as a command line argument, that we should use a variable for the location of AtomicParsley, etc.

Here’s the complete script. I shuld probably get it to merge the two scripts together, but thats probably too polished - the point is, we all need to stop thinking about how to solve problems, and start to think how we can clearly define them. Sometimes you just want a quick hack to achieve something so you can concentrate on something more exciting.

Without this tooling, I’d have had to find this information from atomicparlseys manpages, remind myself of find command line flags, read 17 pages of stack overflow which almost-but-not-quite matched what I was trying to do, etc.

It also commented the scripts. I could learn a few things from ChatGPT myself…

#!/usr/local/bin/bash

# Set the location of AtomicParsley
ATOMICPARSLEY="/usr/local/bin/AtomicParsley"

# Check if a directory is provided as an argument
if [ $# -eq 0 ]; then
    echo "Usage: $0 <directory>"
    exit 1
fi

# Directory provided as the first command line argument
directory="$1"

# Function to check if an M4A file has embedded album art
has_album_art() {
    # Using AtomicParsley to check for album art
    if "$ATOMICPARSLEY" "$1" --textdata | grep -q "Atom \"covr\" contains"; then
        return 0 # Has album art
    else
        return 1 # Does not have album art
    fi
}

# Recursively find all .lrc files in the directory and its subdirectories
find "$directory" -type f -name "*.lrc" | while read lrc_file; do
    # Extract the track number from the .lrc file
    track_number=$(basename "$lrc_file" | cut -c1-2)

    # Find the corresponding .m4a file with the same track number in the directory and its subdirectories
    m4a_file=$(find "$directory" -type f -name "$track_number*.m4a" | head -n 1)

    if [[ -n "$m4a_file" ]]; then
        # Construct new .lrc filename based on .m4a filename
        new_lrc_file=$(dirname "$lrc_file")/$(basename "$m4a_file" | sed 's/\.m4a$/.lrc/')

        # Check if the .lrc file already has the correct name
        if [ "$lrc_file" != "$new_lrc_file" ]; then
            # Rename the .lrc file
            mv "$lrc_file" "$new_lrc_file"
            echo "Renamed '$lrc_file' to '$new_lrc_file'"
        fi
    fi
done

# Find and delete duplicate .m4a files with "(1)" in their names, but only if a file without "(1)" exists
find "$directory" -type f -name "*(1).m4a" | while read duplicate_file; do
    # Construct the name of the corresponding file without "(1)"
    original_file=$(echo "$duplicate_file" | sed 's/ (1)//')

    # Check if the original file exists
    if [ -f "$original_file" ]; then
        rm "$duplicate_file"
        echo "Deleted duplicate file '$duplicate_file'"
    fi
done

# Recursively find all .m4a files and process them
find "$directory" -type f -name "*.m4a" | while read m4a_file; do
    if ! has_album_art "$m4a_file"; then
        # Directory of the current m4a file
        m4a_dir=$(dirname "$m4a_file")

        # Check for 'cover.jpg' in the same directory
        if [[ -f "$m4a_dir/cover.jpg" ]]; then
            # Add album art to the m4a file
            "$ATOMICPARSLEY" "$m4a_file" --artwork "$m4a_dir/cover.jpg" --overWrite
            echo "Added album art to '$m4a_file' from '$m4a_dir/cover.jpg'"
        fi
    fi
done