Notes on Music Management Revision as of Thursday, 15 June 2023 at 16:17 UTC

I used Beets to reorganize my music and get out of the clutches of the ‘Music’ App which is a buggy, unergonomic, unmaintained, embarrassing piece of shit by a company that has a market cap higher than oil companies.

I have about ~24,000 tracks and spent a cumulative of a week importing them into Beets. It uses the lovely and amazing SQLite for its database, and not some bullshit proprietary format, making it possible to write all sorts of UIs on top of it ❀️

Did all of this on macOS 13.2.1 (Ventura)

Working with Beets

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

Basic Configuration

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

import:
    move: yes

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

play:
    command: /opt/homebrew/bin/VLC

convert:
    command: ffmpeg -i $source -ab 320k -ac 2 -ar 48000 -map_metadata 0 -id3v2_version 3 $dest
    extension: mp3

discogs:
    user_token: iHoejCCcOBhqOKZPnFTZlJCcyhZzLyaaryPiOinZ
    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.

Editing Metadata

I use the awesome mp3Tag. 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 my Phone

Created a playlist in the Music App called “Things to Sync” and just synced that to my phone. I tried to get out of Apple’s clutches by using iMazing but it didn’t work: it would stall on the last song to sync and appeared to corrupt the music database. Just stuck with the Finder integration.

Syncing from Beets β†’ Apple Music

I largely gave up on doing this automatically and sync changes manually. It sucks. But that’s how it is. See the “References” section for more notes and frustrations.

In Apple Music β†’ Preferences β†’ Files, uncheck both

You can keep the location of the Music Media Folder wherever you’d like. Here’s my layout:

/Users/nikhil
  β”œβ”€β”€ Library     <- This is my Beets library
  └── Music       <- Apple's Database

Now for Create/Update/Delete operations:

This has worked well so far. It’s manual and against every instinct I have as a programmer but my stuff doesn’t change too often for me to worry about learning AppleScript (which may be dead soon because Apple).

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

Issues with Beets

I’d run beet update and get this weirdness

The Beatles - The Capitol Albums, Vol. 1 - If I Fell (mono)
  albumtype: compilation -> a
  albumtypes: album; compilation -> ['a', 'l', 'b', 'u', 'm', ';', ' ', 'c', 'o', 'm', 'p', 'i', 'l', 'a', 't', 'i', 'o', 'n']

Was able to ‘fix’ this temporarily based on this issue:

beet mbsync 'albumtypes::^\['

References and Notes

The Frustrating State of AppleScript

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.

Here are some cached AppleScripts…

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

Remove Dead Tracks


-- 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 utxt266BÈ} 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