My offline music streaming setup
I like some music alongside whatever I’m doing something else… sometimes. [1] While I typically go with a music streaming service like Spotify, sometimes it is done with my offline music collection.
A music streaming service like Spotify, while nice with the vast library of music you have access to, is not a replacement with the local music collection. Especially with the dangers of a cloud service where if Spotify being down will make your music collection inaccessible for the time.
It’s fun to document my setup every once in a while and the topic to tackle for this post is music management. It is especially nice for those who are into music archival or just want to have a nice digital, well-organized music collection.
For future references, here are the following components assumed for this post as well as its versions.
|
Mopidy
Half of the personal music streaming setup is Mopidy, a music server with a very nice ecosystem. With more of the ecosystem, you can use other extensions that adds sources such as in Spotify, YouTube, Internet Archive, and Funkwhale.
Mopidy is a music server that comes with only a command-line interface which you can see more details on mopidy(1). For a start, Mopidy has a comprehensive quickstart and it is widely available on mainstream Linux distributions such as Debian, Arch Linux, and Fedora. While installing Mopidy varies between distributions, typically installing Mopidy consists of installing Mopidy itself and additional extensions.
-
The one I recommend the most is Iris which is a Mopidy extension that adds a web-based interface for managing and playing music with your Mopidy configuration.
Iris music player -
Another extension I would recommend is mopidy-mpd which adds an MPD-compatible server. With the MPD server enabled, you can then use MPD clients such as Ymuse and ncmpcpp.
-
Lastly, I would install additional extensions for other sources such as YouTube, Internet Archive, and Funkwhale.
After installing the extensions you need, it is time to configure it with a plain-text file. You have to be a bit familiar with the configuration syntax of Mopidy which uses an INI-like format. For an example, here’s my Mopidy configuration barring sensitive parts such as my Last.fm and Spotify account secrets.
~/.config/mopidy/mopidy.conf
[file]
enabled = true
media_dirs =
$XDG_MUSIC_DIR|Music
~/library/music|Library
[http]
hostname = 0.0.0.0
[internetarchive]
browse_limit = 150
collections =
fav-foo-dogsquared
audio
etree
audio_music
audio_foreign
enabled = true
search_limit = 150
[m3u]
base_dir = /home/foo-dogsquared/Music
default_encoding = utf-8
default_extension = .m3u8
enabled = true
playlists_dir = /home/foo-dogsquared/Music/playlists
Since INI format doesn’t have a clear way of specifying list, Mopidy has a format tweak where lists look like the following.
This is seen with the value of |
Among other things, the above configuration does the following…
-
Initiate the Mopidy server at localhost.
-
Adds local source from
$XDG_MUSIC_DIR
and other locations. -
Interacts with the playlists (in M3U format) from
$XDG_MUSIC_DIR/playlists
. -
Adds Internet Archive playlists.
With the configuration complete, you’ll have to start the server by simply running mopidy
.
However, it would be preferable to activate the server at the startup.
If you’re using systemd, you could create a service unit as described from systemd.service(5).
~/.config/systemd/user/mopidy.service
[Install]
WantedBy=default.target
[Service]
ExecStart=mopidy --config %E/mopidy/mopidy.conf
[Unit]
After=network.target
After=sound.target
Description=mopidy music player daemon
Documentation=https://mopidy.com/
|
Then you could enable the service with the following command.
systemctl --user enable --now mopidy.service
Hoorah! Now you have an offline music server! While you could manage your collection in Iris, there are better tools suited for that task which we’ll cover next.
Beets
Beets is a music management system for organizing your music collection — embedding proper metadata, organizing file structure, and more. This is what I use to organize my music directory which makes it usable for other services that expect organized structure such as Mopidy and Gonic. The most notable thing with Beets is it uses metadata from MusicBrainz (at least by default).
Using Beets is very similar with using Mopidy:
-
It may require installing additional plugins for more capabilities.
-
It starts with configuring the program with a plain-text file.
Furthermore, Beets also comes with a command-line interface with a user manual at beet(1).
How you configure Beets is through a plain-text file stored at a default configuration file which you can show with the following command.
beet config --path --default
You can easily edit the configuration file with beet config --default --edit
.
There are a lot of tweaks and behaviors you can change but it is easier to show an example configuration like in the following listing.
~/.config/beets/config.yaml
directory: /home/foo-dogsquared/Music
fuzzy:
prefix: '-'
ignore_hidden: true
import:
group_albums: true
incremental: true
link: false
log: beets.log
move: true
resume: true
library: /home/foo-dogsquared/Music/library.db
match:
ignore_video_tracks: true
plugins:
- acousticbrainz
- chroma
- edit
- export
- fetchart
- fromfilename
- fuzzy
- mbsync
- playlist
- scrub
- smartplaylist
scrub:
auto: true
smartplaylist:
playlist_dir: /home/foo-dogsquared/Music/playlists
playlists:
- name: all.m3u8
query: ''
- name: released-in-$year.m3u8
query: year:2000..2023
relative_to: /home/foo-dogsquared/Music
ui:
color: true
If you’re changing how the auto-tagging match works with its related options, the matches that Beets accepts change significantly.
For example, if you’ve modified |
Among other things in the configuration, you’ll need to explicitly specify which plugins to use (which Beets comes with an extensive list of them). Anyways, here’s what my configuration does.
-
It automatically creates playlists separated by year with the
smartplaylist
plugin. This is what part of my Mopidy configuration gets its playlist from. [2] -
The import process also fetch the album art from MusicBrainz which is nice for music players. This is enabled by
fetchart
plugin. -
The import process also scrubs the metadata in music files with the
scrub
plugin.
After configuring it, you can then start using Beets.
The workflow of Beets is pretty simple: it is a music management system that comes with an library database (configured with library
option).
Beets only considers music files that are included in the library database.
To get started, we have to fill the library database with some music files with the following command.
beet import ~/Downloads/music
Beets will then start to match metadata of the audio files into its sources (such as MusicBrainz and Deezer) and prompts the user what to do next.
beet import
usageBeets, with all of its niceties, have a lot of problems especially with the auto-tagging feature. The most notable thing being the performance which is already acknowledged from the user manual. It is pretty slow especially once you import multiple music files from multiple albums.
Another situation that performance can really hinder is importing larger albums where Beets prompts multiple times. Take note that importing music in the same collection make Beets not only prompt but also merge them which takes some more time. This means you really have to pay attention. Thankfully, Beets can resume import if the process has been interrupted. Still, auto-tagging can be a tedious experience.
Just imagine the previous image but Beets only recognizes one part of the album at a time. Rinse and repeat until all tracks are in the album. But then between each time it prompts there’s an additional prompt to make you either merge the album which takes up more time.
That’s the dilemma for larger albums.
Sounds tedious. Why don’t you just use something like MusicBrainz Picard? It is more integrated and seems to be more responsive as an auto-tagger.
Yeah but I want my well-organized and completely-managed-by-Beets music library though.
Gonic
While this component is not essential if you’re the only user of the setup, it would be nice to have it distributed as a server. One of the most common way to distribute your local collection as a streaming server is with Subsonic which able to attract programs and extensions such as a Mopidy extension.
However, we’re not going to use Subsonic itself as it hasn’t been active in its development which led to forks and compatible servers such as Airsonic, Navidrome, and Funkwhale. Instead, we’re using gonic, one of the Subsonic-compatible servers and it is lightweight enough for my needs compared to the aforementioned servers. More specifically, I like that it has the following features.
-
Listenbrainz scrobbling.
-
It has internet radio support (which some Subsonic clients apparently make use of).
-
Multi-user with their own preferences, data, and whatnot.
The last one being the most important which I could then share the server with other music listeners.
Wait! Why do you have two music servers anyways?
I would like to distribute my audio for my other devices which Mopidy is not suited for. Mopidy is closer to MPD by design which is more focused on playing music on the device running the server.
I could also share the server with other users for their music collection.
Fair point, I guess.
Unlike Mopidy (or the next tool), Gonic is not as available as the other featured tools (as of 2023-05-27) so you’ll have to build the source code yourself. |
Using Gonic first starts with configuring the server. There are different ways to configure it.
-
With passing arguments on the command-line interface.
-
With a plain-text file which have to be indicated with
-config-path
option on the command-line interface. -
With environment variables.
You can use them all if you want to. However, I recommend sticking to one and configuring with a plain-text file as it is easier to transfer the configuration between different servers and with different service managers (if you make use of them).
Anyways, here’s the configuration file for my Gonic server for my system-wide installation.
/etc/gonic/gonic.conf
jukebox-enabled true
listen-addr 192.168.1.1:4747
scan-at-start-enabled true
scan-interval 1
music-path /srv/music
cache-path /var/lib/gonic
podcast-path /var/lib/gonic/podcasts
The above configuration meant to be started with the following command to start the server.
gonic -config-path /etc/gonic/gonic.conf
However, it is better to handle this by the service manager (in this case, systemd). The following systemd service unit is sufficient enough.
/etc/systemd/system/gonic.service
[Install]
WantedBy=default.target
[Service]
ExecStart=gonic -config-path /etc/gonic/gonic.conf
[Unit]
After=network.target
After=sound.target
Description=Gonic server
Documentation=https://github.com/sentriz/gonic
A better example of a systemd service unit can be seen in its source code. You could also improve it by hardening the service which systemd definitely has options listed in systemd.exec(5). For a more comprehensive example of a hardened version of the systemd service, you could look into Gonic service implementation from nixpkgs. |
After setting up the server, just don’t forget to immediately log in to the service with the default account and change the password. This is especially important if you’re going to deploy it for the public.
Then, I have to set up my go-to features: create a user token for Listenbrainz for them recommendations, add a list of internet radios, and subscribe to several podcasts. Finally, all I have to do is to deploy it on the public, set up the networking, and voila! My own little music streaming server suitable to be used for multiple users.
Similar to Mopidy, Gonic doesn’t organize them music folder for you. That is the job more suitable for Beets. |
Overall, I find Gonic to be a very nice lightweight music streaming server. At this point, to make use of the service, you’ll have to use a Subsonic client. My client of choice is Ultrasonic but DSub is a close contender too.
Setting up in home-manager
If you’ve seen my recent writings, you would know I’m a Nix enthusiast. Fortunately, there’s a way to easily reproduce the music player setup with home-manager which is nice for user-specific configurations. In fact, home-manager does come with Nix modules to setup both Beets and Mopidy.
Here’s the equivalent home-manager configuration for my Mopidy setup with additional extensions to be installed…
let
musicDir = config.xdg.userDirs.music;
playlistsDir = "${musicDir}/playlists";
in
{
services.mopidy = {
enable = true;
extensionPackages = with pkgs; [
mopidy-beets
mopidy-funkwhale
mopidy-internetarchive
mopidy-iris
mopidy-local
mopidy-mpd
mopidy-mpris
mopidy-youtube
];
settings = {
http = {
hostname = "0.0.0.0";
};
file = {
enabled = true;
media_dirs = [
"$XDG_MUSIC_DIR|Music"
"~/library/music|Library"
];
};
internetarchive = {
enabled = true;
browse_limit = 150;
search_limit = 150;
collections = [
"fav-foo-dogsquared"
"audio"
"etree"
"audio_music"
"audio_foreign"
];
};
m3u = {
enabled = true;
base_dir = musicDir;
playlists_dir = playlistsDir;
default_encoding = "utf-8";
default_extension = ".m3u8";
};
};
};
}
…for the Beets configuration,
let
musicDir = config.xdg.userDirs.music;
playlistsDir = "${musicDir}/playlists";
in
{
# My music player setup, completely configured with Nix!
programs.beets = {
enable = true;
settings = {
library = "${musicDir}/library.db";
plugins = [
"acousticbrainz"
"chroma"
"edit"
"export"
"fetchart"
"fromfilename"
"fuzzy"
"mbsync"
"playlist"
"scrub"
"smartplaylist"
];
ignore_hidden = true;
directory = musicDir;
ui.color = true;
import = {
move = true;
link = false;
resume = true;
incremental = true;
group_albums = true;
log = "beets.log";
};
match.ignore_video_tracks = true;
# Plugins configuration.
fuzzy.prefix = "-";
scrub.auto = true;
smartplaylist = {
relative_to = musicDir;
playlist_dir = playlistsDir;
playlists = [
{
name = "all.m3u8";
query = "";
}
{
name = "released-in-$year.m3u8";
query = "year:2000..2023";
}
];
};
};
};
}
…and for the Gonic service setup which you could make it accessible through a VPN setup like Wireguard or Tailscale.
let
musicDir = config.xdg.userDirs.music;
playlistsDir = "${musicDir}/playlists";
in
{
systemd.user.services.gonic = {
Unit = {
After = [ "network.target" "sound.target" ];
Description = "Gonic server";
Documentation = [ "https://github.com/sentriz/gonic" ];
};
Service.ExecStart = "${lib.getBin pkgs.gonic}/bin/gonic -config-path %E/gonic/gonic.conf -music-path %h/Music -cache-path %C/gonic -podcast-path %C/gonic/podcasts";
Install.WantedBy = "default.target";
};
}
You could create much more comprehensive offline music player with home-manager. The following listing is an example of such setup.
{ config, lib, pkgs, ... }:
let
musicDir = config.xdg.userDirs.music;
playlistsDir = "${musicDir}/playlists";
in
{
# My music player setup, completely configured with Nix!
programs.beets = {
enable = true;
settings = {
library = "${musicDir}/library.db";
plugins = [
"acousticbrainz"
"chroma"
"edit"
"export"
"fetchart"
"fromfilename"
"fuzzy"
"mbsync"
"playlist"
"scrub"
"smartplaylist"
];
ignore_hidden = true;
directory = musicDir;
ui.color = true;
import = {
move = true;
link = false;
resume = true;
incremental = true;
group_albums = true;
log = "beets.log";
};
match.ignore_video_tracks = true;
# Plugins configuration.
fuzzy.prefix = "-";
scrub.auto = true;
smartplaylist = {
relative_to = musicDir;
playlist_dir = playlistsDir;
playlists = [
{
name = "all.m3u8";
query = "";
}
{
name = "released-in-$year.m3u8";
query = "year:2000..2023";
}
];
};
};
};
services.mopidy = {
enable = true;
extensionPackages = with pkgs; [
mopidy-beets
mopidy-funkwhale
mopidy-internetarchive
mopidy-iris
mopidy-local
mopidy-mpd
mopidy-mpris
mopidy-youtube
];
settings = {
http = {
hostname = "0.0.0.0";
};
file = {
enabled = true;
media_dirs = [
"$XDG_MUSIC_DIR|Music"
"~/library/music|Library"
];
};
internetarchive = {
enabled = true;
browse_limit = 150;
search_limit = 150;
collections = [
"fav-foo-dogsquared"
"audio"
"etree"
"audio_music"
"audio_foreign"
];
};
m3u = {
enabled = true;
base_dir = musicDir;
playlists_dir = playlistsDir;
default_encoding = "utf-8";
default_extension = ".m3u8";
};
};
};
programs.ncmpcpp = {
enable = true;
mpdMusicDir = musicDir;
};
systemd.user.services.gonic = {
Unit = {
After = [ "network.target" "sound.target" ];
Description = "Gonic server";
Documentation = [ "https://github.com/sentriz/gonic" ];
};
Service.ExecStart = "${lib.getBin pkgs.gonic}/bin/gonic -config-path %E/gonic/gonic.conf -music-path %h/Music -cache-path %C/gonic -podcast-path %C/gonic/podcasts";
Install.WantedBy = "default.target";
};
home.stateVersion = "23.05";
}
Just install or activate home-manager with the above configuration fragment and you should be able to reproduce my configuration in a snap. While unprivileged user services can be used for network-wide deployment as long as the networking is configured right, home-manager is oriented towards desktop usage. You’ll be missing out on setting up things (mainly networking) so configuring it through NixOS would be better suited for this task.
Setting up in NixOS
We could also set a music streaming server with NixOS which is more suitable for servers compared to home-manager which is oriented towards desktop usage.
Here’s how we would set up with Mopidy…
let
musicDir = "/srv/music";
playlistsDir = "${musicDir}/playlists";
in
{
services.mopidy = {
enable = true;
extensionPackages = with pkgs; [
mopidy-iris
mopidy-local
mopidy-mpd
mopidy-mpris
mopidy-youtube
];
configuration = ''
[http]
hostname = 172.23.0.1
port = 6669
[file]
enabled = true
media_dirs =
${musicDir}|Music
[m3u]
enabled = true
base_dir = ${musicDir}
playlists_dir = ${playlistsDir}
default_encoding = utf-8
default_extension = .m3u8
'';
};
}
…and with Gonic.
let
musicDir = "/srv/music";
playlistsDir = "${musicDir}/playlists";
in
{
services.gonic = {
enable = true;
settings = {
listen-addr = "172.23.0.1:4747";
cache-path = "/var/cache/gonic";
music-path = [ musicDir ];
podcast-path = "/var/cache/gonic/podcasts";
jukebox-enabled = true;
scan-interval = 1;
scan-at-start-enabled = true;
};
};
}
As with Beets, you can simply alias it and pass the path of the custom configuration file. [3] Though, the file has to have appropriate permissions to easily access it for all.
let
beetsOverride = pkgs.writeScriptBin "beet" ''
${pkgs.beets}/bin/beet --config=${./config/beets/config.yml}
'';
in
{
environment.systemPackages = [ beetsOverride ];
}
Anyways, here is the complete NixOS configuration for future reference which you can activate it with nixos-rebuild
.
{ config, lib, pkgs, ... }:
let
musicDir = "/srv/music";
playlistsDir = "${musicDir}/playlists";
beetsOverride = pkgs.writeScriptBin "beet" ''
${pkgs.beets}/bin/beet --config=${./config/beets/config.yml}
'';
in
{
environment.systemPackages = [ beetsOverride ];
services.mopidy = {
enable = true;
extensionPackages = with pkgs; [
mopidy-iris
mopidy-local
mopidy-mpd
mopidy-mpris
mopidy-youtube
];
configuration = ''
[http]
hostname = 172.23.0.1
port = 6669
[file]
enabled = true
media_dirs =
${musicDir}|Music
[m3u]
enabled = true
base_dir = ${musicDir}
playlists_dir = ${playlistsDir}
default_encoding = utf-8
default_extension = .m3u8
'';
};
services.gonic = {
enable = true;
settings = {
listen-addr = "172.23.0.1:4747";
cache-path = "/var/cache/gonic";
music-path = [ musicDir ];
podcast-path = "/var/cache/gonic/podcasts";
jukebox-enabled = true;
scan-interval = 1;
scan-at-start-enabled = true;
};
};
system.stateVersion = "23.05";
}
You could set this up within a VPS. If you’re not comfortable with setting this up for the public, you could easily require the service to be accessed through a VPN. You can even set up a domain name that is only accessed through the VPN.