Notes on Music Management Revision as of Saturday, 11 February 2023 at 18:31 UTC

I used Beets to reorganize my music and get out of the clutches of iTunes (or the ‘Music’ App). I have about ~22,000 tracks and spent a cumulative of a week importing them into Beets. It uses the lovely and amazing SQLite for its database making it possible to write all sorts of UIs on top of it ❤️

Did all of this on macOS 12.6 (Monterey).

Working with Beets

Just random notes and not a comprehensive guide. The documentation is great.

Basic Configuration

directory: ~/Music
library: ~/Music/library.db
ignore:
    - '*.flac'

import:
    move: yes

plugins:
    - convert
    - discogs
    - duplicates
    - edit
    - embedart
    - fetchart
    - fromfilename
    - fuzzy
    - info
    - lastgenre
    - lyrics
    - missing
    - web

# Meant to convert upon import but didn't work for me...
convert:
    command: ffmpeg -i $source -ab 320k -ac 2 -ar 48000 -map_metadata 0 -id3v2_version 3 $dest
    extension: mp3


discogs:
    user_token: LolToken
    index_tracks: yes

edit:
    itemfields:
        - artist
        - album
        - album_id
        - track
        - title
        - genre

Searching and Listing Things

# List things with formatting
beet ls -f '$id - $track/$tracktotal - $album - $title' Massive Attack 100th

# List the exact album
# https://github.com/beetbox/beets/issues/4371
beet ls artist:Pink Floyd album:~'The Wall'

Database Schema

beet fields will give you a list of all the fields/columns you’ll find the database. You can add your own too! I used the excellent litecli for a lot of explorations.

Syncing with my Phone

I don’t use Music to sync songs to my phone and rely on iMazing instead. I don’t use Music to edit Metadata and use the awesome mp3Tag instead. If on Linux, beet edit with your favorite editor will work just fine (except for album art… not sure how you’d do that at the CLI).

Syncing with Apple Music

Add Tracks

This is AppleScript. You run this as a hook upon adding a song. The argument is the absolute path to the song. Re-running this won’t add the song twice.


-- Reference: https://randomgeekery.org/post/2017/10/beets-and-itunes/

on run (argv)
    tell application "Music"
        set filename to (argv's item 1 as string)
        try
            set trackRef to filename
            refresh trackRef
        end try
    end tell
end run

Refreshing the Library

Especially when you delete a song in beets and want that reflected in your Music app. This is slow but whatever.

-- Reference:
-- https://www.leawo.org/entips/remove-dead-tracks-in-itunes-1114.html

tell application "Music"
    set mainLibrary to library playlist 1
    set totalTracks to count of file tracks of mainLibrary
    set deleted_tracks to 0

    log "There are " & totalTracks & " tracks in the library."
    log "Scanning... this will take a while."

    repeat with t from totalTracks to 1 by -1
        set the_track to file track t of mainLibrary

        if the_track's location is missing value then
            log "MISSING: " & the_track
            delete the_track
            set deleted_tracks to deleted_tracks + 1
        end if
    end repeat

    log "I deleted " & deleted_tracks & " tracks."
end tell

Dealing with FLAC Files

I converted these to 320Kbps MP3 for maximum comatibility. X Lossless Decoder is an OG converter for macOS but I ended up using the venerable ffmpeg.

# Convert FLAC to 320kbps MP3
# I kept the FLAC files around too...
for i in *.flac
do
    ffmpeg  -i "$i" \
            -ab 320k \
            -map_metadata 0 \
            -id3v2_version 3 \
            "`basename "$i" .flac`.mp3"
done

To split FLAC files, I used xACT will split the FLAC files based on CUE files for macOS. It uses shntool for this. So if you do this at the command-line,

# Split FLAC files according to CUE files (macOS)
brew install cuetools flac ffmpeg shntool
shnsplit -o flac -f file.cue file.flac

# Use cuetag to tag the newly split files based on information from the CUE files
# You get this from cuetools but here's a bash script:
# https://github.com/gumayunov/split-cue/blob/master/cuetag
cuetag.sh file.cue split-track*.flac

References and Notes

You can script things in macOS in AppleScript or JavaScript. The documentation is old and pretty garbage and you’ll have to rely on Google and SO to do things.

If, like me, you’re excited that you can write stuff in JavaScript, calm your horses. There’s no code-completion, no types, and no IDE support in XCode since they removed the AppleScript template in v14 (you have to copy it over from v13). This has led to at least one person on the internet asking if Apple will deprecate AppleScript. And a petition. And this salty-ass reply from a mature dev exploring the history of AppleScript.

It’s all rather shitty. Avoid using the Music app as much as possible and use iMazing to transfer stuff to your iPhone until that company sadly announces that Apple has blocked them from doing their thing as well.

Remove Dead Tracks - Cached


-- remove dead files
-- Reference: https://gist.github.com/aleemb/97b4f5f8510f397c4a08

property progress_factor : 500

tell application "Music"
    display dialog "Super Remove Dead Tracks" & return & return & Â
        "Removes tracks whose files are missing or deleted, so-called" & Â
        "\"dead\" tracks designated with (!) next to the track name." buttons Â
        {"Cancel", "Proceed..."} default button 2

    set mainLibrary to library playlist 1
    set totalTracks to count of file tracks of mainLibrary
    set all_playlists to user playlists whose smart is false -- don't delete Smart Playlists later

    set deleted_tracks to 0
    set all_checked_tracks to 0
    set countem to ""

    set oldfi to fixed indexing
    set fixed indexing to true

    repeat with t from totalTracks to 1 by -1

        try
            set this_track to file track t of mainLibrary
            -- set _duration to duration of this_track
            -- set _title to name of this_track

            set loc to "." & this_track's location

            if loc is missing value or loc contains ".Trash" or _title is missing value then
                display dialog "Deleting: " & _title as string
                delete this_track
                set deleted_tracks to deleted_tracks + 1
            end if

            set all_checked_tracks to all_checked_tracks + 1

            if frontmost then
                if (progress_factor is not 0) and (all_checked_tracks mod progress_factor) is 0 then
                    if deleted_tracks is greater than 0 then Â
                        set countem to (deleted_tracks & " dead tracks removed so far...")
                    if frontmost is true then display dialog (all_checked_tracks as string) & Â
                        " tracks checked..." & Â
                        return & countem buttons {"Cancel", Çdata utxt266} giving up after 1
                end if
            end if
        end try

    end repeat

    set fixed indexing to oldfi

    repeat with this_playlist in all_playlists
        if (get count of tracks of this_playlist) is 0 then
            try
                delete playlist this_playlist
            end try
        end if
    end repeat

    if deleted_tracks is greater than 0 then
        set ps to " was"
        if deleted_tracks is not 1 then set ps to "s were"
        display dialog "Removed " & deleted_tracks & " dead track" & ps & Â
            "." buttons {"Thanks"} default button 1 with icon 1
    else
        -- if gave up of (display dialog "No dead tracks found." buttons {"Thanks"} Â
        --  default button 1 with icon 1 giving up after 15) is true then error number -128
    end if

end tell