qlyoung's wiki

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 tech company to rent temporary access their music library which they pay to license from a label who gives some tiny fraction of their revenue to the artist, I prefer a more traditional model where an artist makes some music, you pay the artist for a copy, and then you own that copy. That way the artist gets the 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 synced up on all of them can be a little daunting. This article roughly depicts how I do that.

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 -.-> |nfs| 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.

For my desktop and laptop, they have the filesystem where my music library resides mounted using NFS. Once the library is mounted, it's accessible like any other local directory, and then I use my player of choice to play the music. For my laptop (well, all my devices) this works even when I am on the other side of the planet because network access is via Tailscale; see personal infrastructure to read more about how that is set up. Latency has never been a issue, but when I'm traveling I'm using my phone for music anyway. My desktop is also on Tailscale but thanks to insane magic it routes over LAN, so there is no latency penalty.

The phone is a little trickier. Ideally I would like to run a sync daemon in the background on my server and phone that syncs the music directory to the device, but that's not how things work on iOS. For one, background daemons cannot really exist on iOS (there are exceptions to this - iCloud for example - but nothing worth using). For two, there is not really a place to put the music. Due to how sandboxing works on iOS, you need to pick an app to put the music “into”. Since only apps can write to app filesystems, the upshot is that you need an app that also has its own syncing service built in. This is heretical, as a music player should be able to focus on playing music and allow a syncing application to handle data sync, but that's how Apple designed it. That is why the app store has a bunch of terrible apps with names like “Network music player ULTIMATE” that have various support for playback and/or sync to/from NFS, Samba, WebDAV, and 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? There has to be a good app! The answer is that in addition to what I just said about the apps sucking, network conditions on mobile are variable enough that the experience of streaming from a home server sucks. The only reason it works so well with Spotify is that - guessing here - Spotify has global datacenter and edge presence all over the world, like any major web service, which papers over the network. Also, you can't play your music on the airplane. Pinning is not acceptable because I don't want to choose what music to listen to in advance. I don't listen to music like that.

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 is 1) Windows only, 2) closed source and 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 the same server that has my music library and mount the share with the music into the VM.

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
Panorama theme by desbest
music_management.txt · Last modified: 2024/02/14 05:49 by qlyoung
CC Attribution-Noncommercial-Share Alike 4.0 International Except where otherwise noted, content on this wiki is licensed under the following license: CC Attribution-Noncommercial-Share Alike 4.0 International