Table of Contents

music management

I'm a big music enjoyer. What I'm not a big enjoyer of is streaming services. Maybe I'll write another article about that, but the short of it is that instead of paying a company to rent temporary access to their music library, which they pay to license from a label who in turn pays only some tiny fraction of their revenue to the artist, I prefer a more traditional model:

  1. artist makes music
  2. you pay the artist for a copy
  3. you own that copy

That way the artist gets money and you own something in return for your money.

One of the benefits of owning music is that you can put it on whatever device you want and use whatever program you prefer to play it. However, if you have multiple devices getting your music collection available on all of them becomes an exercise in file management. This article roughly depicts how I solve it.

Methods

Here's a chart that shows a rough outline of data flow:

flowchart TD
    Source -.-> |download| staging
    W["Windows VM"]
    subgraph server["__server"]
        staging --> |beet import| library
        library --> W
    end
    subgraph devices["devices_____"]
        library -.-> |syncthing| desktop(("fa:fa-computer desktop")) & laptop(("fa:fa-laptop laptop"))
        W -.-> |tunefusion| phone(("fa:fa-mobile-phone phone"))
    end
    click phone "https://www.dbpoweramp.com/tunefusion.htm"

In short:

  1. Acquire the music from somewhere; for me, I usually buy it on Bandcamp
  2. Download it to a “staging” directory on my home server
  3. Log into the server and run beet import, pointing it at the staging directory. This adds the music to my library database, cleans up tags, pulls album art, properly names the files and copies them into my “Artist/Album/<files>” library directory structure, applying my chosen file naming scheme. beets slaps.

The rest of the diagram depicts how the 3 devices I play my library on end up with access to my music.

Desktop & Laptop

My server, desktop and laptop all run Syncthing. The music directory is synchronized to all devices using that.

In the past, both of these devices mounted the directory containing music via NFS. Since they're all on a Tailnet together this worked as long as both the server and client were both online, regardless of the routing environment. This worked pretty well - latency was never an issue, and in any case I rarely needed to play from my laptop since I usually use my phone when I'm outside my home network. However, I didn't like the network requirement and realized disk space is plentiful while bandwidth is a scarce commodity. Since I generally frown upon streaming when it isn't necessary I put my money where my mouth was and switched to file synchronization.

For playback on the computer, I prefer tauon music box.

Phone

The phone is a little trickier. Ideally I would use the same approach as for my desktop and laptop - run a sync program to keep the music library up to date on my iPhone disk. Unfortunately, that's not how things work on iOS, for two reasons:

  1. background daemons cannot really exist on iOS
  2. the concept of a filesystem that is shared between apps does not exist on iOS

Regarding backgrounding, in general iOS is very strict about apps performing any work in the background. Since sync programs are designed around running in the background, this more or less precludes the file synchronization strategy. It's worth noting that this could be worked around using iCloud, but since I don't use iCloud, that doesn't work for me.

Even if a sync program was viable on iOS, we would hit another blocker. iOS does not have the concept of a shared filesystem. Apps are only able to write to their own sandboxed filesystems. Consequently any files downloaded by a sync app would not be accessible by a music player app.

The upshot is that to solve this problem you need a music playback app that also has its own syncing service built in. Obviously this is heretical. A music player should focus solely on playing music, while a syncing application should handle data sync. The result of that being impossible is the app store has a bunch of terrible apps with names like “Network music player ULTIMATE” that have varying levels of support for playback and/or sync to/from various data sources - Samba, WebDAV, whatever else you can think of. I've tried most of them and they all suck.

I can already hear you saying, “why don't you just play back your music over the network using a network player”? In addition to the available apps sucking, network conditions on mobile are variable enough that streaming from a home server results in a generally poor experience. Industrial streaming services like Spotify have to go to extreme lengths to paper over the network enough to deliver a good experience. “Pinning” - where you stream but select specific items to keep locally on disk - doesn't really work for me because I don't want to choose what music to listen each time I anticipate a no-network scenario.

Anyway, as luck would have it, the best music player on iOS also has the best sync solution I've seen. The downside is that the companion program that runs on your server

  1. is Windows only
  2. is closed source
  3. costs money

However, it is surprisingly full featured and works very well. Since it's Windows only, I run it inside a windows VM on my hypervisor (which also hosts the file server with my music library). The VM has the music directory mounted from the file server via Samba.

The end result is that every time I open the foobar2000 app on iOS, any new music in my library downloads to my device. After that it's available for local playback. Since my phone is also on Tailscale, this works anywhere.

Historical methods

I used to do it a much more complicated way than depicted above because I had an Android phone with not enough storage to store my lossless music collection. Android meant I could run background file syncing utilities and not enough storage meant that I had to transcode my collection to something lossy in order to crunch it down small enough to fit on my phone. Buying an iPhone with 512gb of storage meant that 1) I lost the ability to run any kind of background syncing software because iOS doesn't really allow daemons to exist (unless Apple made them) and 2) I no longer needed to transcode as my music collection is only ~115gb which fits on my phone's internal storage. Thus all of the automatic cron jobs to do periodic transcoding and sync via syncthing etc are no more.

These are the unintelligible notes I took about how I used to do it, replete with ascii diagrams from a time before I caved a little bit on my static site elitism and just used mermaid:

1. Tx from source to local staging directory 2. `beet import` from staging directory into mounted remote share[0]

                 { source }
                     |
                     .
                    ---   (net) <1>
                     .
                     |
                     v
              [ local:music ]
                     |
                     .
            beet    ---   (net) <2>
           import    .
                     |
                     v
      [ remote:music => local:remote/music]

3. cron job on remote periodically copies new files into a sync directory,

 transcoding any lossless files to `-q6` ogg vorbis to reduce size[1]
             [ remote:music ]
                     |
                     |
    music-sync.sh    |    @ 2hr <3>
                     |
                     v
           [ remote:music-sync ]

4. sync directory shared to all devices via syncthing[2]

           [ remote:music-sync ]
                     |
                     |
                     .
        syncthing   ---   (net) <4>
                     .
                    ...
                   . . .
                  .  .  .
                 .   .   .
                /    |    \
               v     v     v
            phone  laptop  idk

[0] <https://beets.io/>

[2] <https://syncthing.net/>

#!/usr/bin/fish
set MUSICDIR "./music/"
set SYNCDIR  "./music-sync"
 
for dir in (find "$MUSICDIR" -type d | cut -d'/' -f3-)
    mkdir -p "$SYNCDIR/$dir"
end
 
for file in (find "$MUSICDIR" -type f -name '*.flac' -o -name '*.mp3' -o -name '*.ogg' | cut -d'/' -f3-)
    set ifile (echo "$MUSICDIR/$file")
    switch $file
    case "*.flac"
        set ofile (echo "$SYNCDIR/"(echo "$file" | sed "s/flac/ogg/"))
        if test -e "$ofile"
            echo "$ofile exists; skipping"
            continue
        end
        echo ">> Transcoding '$ifile' to '$ofile'"
        oggenc -q6 -o "$ofile" "$ifile"
    case "*"
        set ofile (echo "$SYNCDIR/$file")
        if test -e "$ofile"
            echo "$ofile exists; skipping"
            continue
        end
        echo ">> Copying '$ifile' to $ofile"
        cp "$ifile" "$ofile"
    end
end