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
- Discogs was extremely helpful, especially with Indian stuff and radio shows.
- Set
EDITOR="subl -w"
before runningbeet edit foo
since multiple cursors save a lot of time when editing tag data.
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.
- Apple’s dumb MusicDB format
- Remove Dead Tracks (based on this script that’s available for $1.99)
- AppleScript References: One, Two, Three, Four, Five
- A Collection of macOS Automation Sites
- Scripting Terminology in AppleScript
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 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