nghe
nghe copied to clipboard
An OpenSubsonic implementation in Rust
Nghe
An implementation of OpenSubsonic API in Rust
-
Nghe
- Features
- Getting started
- Client
-
Configuration
- Server
- Database
- Artist
-
Parsing
- Song
- Album
- Id3v2
- Number and total
- Scan
- Transcoding
- Art
- S3
-
Scan process
- How a song is uniquely identified ?
-
Scan mode
- Quick
- Full
- Force
- How an artist is uniquely identified ?
- How an album is uniquely identified ?
-
Permission model
- Access to a song-level resource
- Access to an artist or album level resource
- Compilation album
- Roadmap
Features
- Written in Rust with performance in-mind.
- All tags are multiple by default (artists, album artists, languages, etc).
- Well-tested and highly customizable.
- Well-defined permission model with music folders.
- Multi-platform, runs on Linux, FreeBSD, MacOS and Windows. Docker images with two variants GNU or MUSL are also provided.
- Bridging with
ffmpeg c api
for in-memory transcoding and smooth stream experience. Most common formats (opus, mp3, acc, wav, etc) are supported. Does not required any manual configuration beforehand, justmaxBitRate
andformat
in the request parameters are enough. - Synchoronized lyrics from external
lrc
files. - AWS S3 compatible storage support. Tested with Minio for every commit.
Getting started
The easiest way for getting started is using docker. Below is a docker-compose.yaml
example. A random hex key can be generated using openssl rand -hex 16
.
services:
nghe:
image: ghcr.io/vnghia/nghe-musl:0.3.0
ports:
- "3000:3000"
restart: unless-stopped
environment:
NGHE_DATABASE__URL: postgres://postgres:postgres@db:5432/postgres
NGHE_DATABASE__KEY: a20eb15ac92cabfd96b81fb154b16357
volumes:
- /your-music-folder/:/data/music/:ro
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
The minimum required postgres version is 16. Alternatively, you can also download corresponding binary with your OS and the frontend package in the release section.
Once the server is running, go to [your-server-url]/setup
to setup a first admin account. After that, login and go to the folders menu on the right side of the screen. You can add a new music folder from there, hit the scan button, choose one scan mode (more detail in scan process) and start using the server while your media files are being scanned.
Client
This server works best with Symfonium and Airsonic since they support multiple values well enough.
Configuration
All configurations can be set by environment variable with a NGHE_
prefix and a __
between each level of inheritance. For example, the config database.url
is correspondent to NGHE_DATABASE__URL
.
Server
Subkey | Meaning | Default value | Note |
---|---|---|---|
host | IP host to bind the server to | 127.0.0.1 |
:: for Docker |
port | Port to bind the server to | 3000 | |
frontend_dir | The directory contains the pre-built frontend | $PWD/frontend/dist/ |
/app/frontend/ for Docker |
Database
Subkey | Meaning | Default value | Note |
---|---|---|---|
url | URL to connect to the database | ||
key | A 32-characters hex string to use as encryption key for sensetive data (password) |
Artist
Subkey | Meaning | Default value | Note |
---|---|---|---|
ignored_articles | Articles to ignore while building artist indexes | "The An A Die Das Ein Eine Les Le La" |
Parsing
Parsing config allows you to tells the server exactly where to read your songs metadata. Currently, belows tags are supported.
Subkey | Meaning | Id3v2 | VorbisComments | Note |
---|---|---|---|---|
song | Subconfiguration for parsing song | song | ||
album | Subconfiguration for parsing album | album | ||
artist | Artist names | TPE1 | ARTIST | |
album_artist | Album artist names | TPE2 | ALBUMARTIST | |
track_number | Track number | TRCK | TRACKNUMBER | number and total |
track_total | Track Total | TRACKTOTAL | number and total | |
disc_number | Disc number | TPOS | DISCNUMBER | number and total |
disc_total | Disc Total | DISCTOTAL | number and total | |
language | Languages | TLAN | LANGUAGE | Should be a ISO 639-3 or 639-1 code, not 639-2 |
genre | Genres | TCON | GENRE | |
artist_mbz_id | Artist musicbrainz id | "MusicBrainz Artist Id" | MUSICBRAINZ_ARTISTID | Should be specified only if you have only one artist in the tag because the order is not perserved while parsing |
artist_mbz_id | Artist musicbrainz id | "MusicBrainz Album Artist Id" | MUSICBRAINZ_ALBUMARTISTID | Should be specified only if you have only one album artist in the tag because the order is not perserved while parsing |
Song
Some fields in this table are not official and usually used to distinguish between date-related information of albums and songs. If you have don't have that information, just leave it as default.
Subkey | Meaning | Id3v2 | VorbisComments | Note |
---|---|---|---|---|
name | Song name | TIT2 | TITLE | |
date | Song recording date | TRCS | SDATE | Set null to completely disable parsing this field |
release_date | Song release date | TSRL | SRELEASEDATE | Set null to completely disable parsing this field |
original_release_date | Song original release date | TSOR | SORIGYEAR | Set null to completely disable parsing this field |
mbz_id | Song musicbrainz id | "MusicBrainz Release Track Id" | MUSICBRAINZ_RELEASETRACKID |
Album
Subkey | Meaning | Id3v2 | VorbisComments | Note |
---|---|---|---|---|
name | Album name | TALB | ALBUM | |
date | Album recording date | TDRC | DATE | Set null to completely disable parsing this field |
release_date | Album release date | TDRL | RELEASEDATE | Set null to completely disable parsing this field |
original_release_date | Album original release date | TDOR | ORIGYEAR | Set null to completely disable parsing this field |
mbz_id | Album musicbrainz id | "MusicBrainz Album Id" | MUSICBRAINZ_ALBUMID |
Id3v2
Id3v2 key is treated as two different ways depending on its length:
- If you supply a 3 or 4 characters string, it will be treated as a frame id. For example TIT2.
- Otherwise, it will be treated as an user text key in the frame TXXX. For example "MusicBrainz Release Track Id".
In additional to those configurations above, id3v2 also has below configuration.
Subkey | Meaning | Default value | Note |
---|---|---|---|
separator | Separator for multiple values in id3v2.3 | '/' | This is only applicable with id3v2.3, id3v2.4 will always be splited by '\0' (or '\\' if you are using mp3tag) |
Number and total
You can set track/disc number and total by three ways below:
- Setting fields
track_number
,track_total
, etc normally. Unfortunately this does not work on id3v2. - Using only
track_number
and set it to{track_number}/{track_total}
. This setstrack_number
andtrack_total
to the corresponding numbers after spliting and works with all formats. The same thing holds fordisc_number
. - Using only
track_number
and set it to{character}{track_number}
likeA1
,B10
. This sets thedisc_number
to the order of that character in the alphabet andtrack_number
to the following number.track_total
anddisc_total
will be none. This format is encountered while parsing vinyl records.
Scan
Scan process has two main threads, one thread (the walking thread) will be responsible for walking directories in the filesystem and send back the result to the second thread (the parsing thread), who is responsible for parsing each file and updating information in the database. More in scan process.
You should tweak this carefully to find the optimized value for your system. Sometimes, running concurrently is not the fastest option.
Subkey | Meaning | Default value | Note |
---|---|---|---|
parallel | If the walking thread should spawn more threads and run concurrently. | false | |
channel_size | The maximum number of results that can be sent back to the parsing thread. If the results queue is full, the walking threads will be blocked until the queue has an empty slot | 10 | |
pool_size | The maximum number of threads that the parsing thread can spawn to process the result | 10 |
Transcoding
Subkey | Meaning | Default value | Note |
---|---|---|---|
buffer_size | Buffer size to allocate for custom AVIOContext |
32 * 1024 | |
cache_dir | The cache directory to save transcoding results | $TMPDIR/nghe/cache/transcoding |
Set null or a non-absolute path to completely disable parsing caching |
Art
Subkey | Meaning | Default value | Note |
---|---|---|---|
artist_dir | The directory to save artist cover arts | $TMPDIR/nghe/art/artist |
Set null or a non-absolute path to completely disable song cover art extract |
song_dir | The directory to save song cover arts | $TMPDIR/nghe/art/song |
Set null or a non-absolute path to completely disable song cover art extract |
Lastfm
Subkey | Meaning | Default value | Note |
---|---|---|---|
key | Lastfm key to fetch information |
Spotify
Subkey | Meaning | Default value | Note |
---|---|---|---|
id | Spotify client id to fetch information | ||
secret | Spotify client secret to fetch information |
S3
Credentials, region and endpoint configurations should be set by standard AWS environment variables. Authentication by profile and imds are also supported.
Subkey | Meaning | Default value | Note |
---|---|---|---|
enable | Enable S3 integration | ||
use_path_style_endpoint | Use path style endpoint instead of virtual host | whether $AWS_USE_PATH_STYLE_ENDPOINT is empty or not |
|
presigned_url_duration | Duration (in minutes) of the presigned url for transcoding | 15 | |
stalled_stream_grace_preriod | Grace period for stalled stream protection | 0 | Set 0 to disable stalled stream protection |
connect_timeout | The maximum time (in seconds) it takes to initiate a socket connection. | 5 |
Scan process
How a song is uniquely identified ?
A song is uniquely identified either by:
- Its music folder and relative path (to its music folder).
- Its music folder, hash and size. It means that you can not have duplicated songs in the same music folder. If a duplicated songs is detected, the path will be updated to the latest encountered path. It allows you to move freely your songs in the same music folder, only its path will be updated in the database.
Duplication on two different music folders are is allowed and will be treated as two seperate songs.
Scan mode
Quick
The song will be first checked by its last modified time and then process the same as full if its updated at time is sooner or it hasn't been added into the database. Otherwise mark it as scanned and move to the next song. This mode is particularly useful for cloud storage service.
Full
As describe above, if an already identified song is scanned (by checking relative path, hash and size), the scanning process will just mark the file as scanned and skip that file. In the end of the scanning process, all old songs and related informations (artists/albums/genres/etc) inside that music folder that are not scanned will be deleted.
Force
Same as full but will try parsing the file regardless if it is identified or not. This mode is useful if there are new metadata that added into the scanning process.
How an artist is uniquely identified ?
An artist is uniquely identified either by:
- Their musicbrainz id
- Their name if their musicbrainz id is empty.
If you have duplicated artists shown in your client, you should check for the corresponding musicbrainz id field.
How an album is uniquely identified ?
An album is uniquely identified either by:
- Their musicbrainz id
- Their name, date, release date, original release date if their musicbrainz id is empty.
If you have duplicated albums shown in your client, you should check for the corresponding musicbrainz id and date fields.
Permission model
As describe in scan process, songs are tied to a specific music folder but artists and albums are globally identified. There will be two scenarios.
Access to a song-level resource
In this case, user is either allowed or denied depending on their allowed music folders. Song-level resource is the song itself, the lyrics, the cover art.
Access to an artist or album level resource
In this case, user will only see a combination of allowed song-level resources, not the whole thing.
For example, if an artist has 10 songs in folder1 and 20 songs in folder2:
- User with permission on both folders will see that artist has 30 songs.
- User with permission on folder1 will only see first 10 songs.
- User with permission on folder2 will only see last 20 songs.
- User with no permission on both folder will not see the artist in the first place and get a not found error if they are trying to access it by manully specifying the id.
The same thing holds true for albums. For example, if an album has 2 songs in 2 folders with different cover arts:
- User with permission on both folders will see the first cover art sorted by the smallest disc number and track number (track 1 disc 1 of the album).
- User with permission on folder1 will only see the first cover art.
- User with permission on folder2 will only see the second cover art.
Nested music folder
Nested music folders are allowed and each folder will be treated as a separate unrelated folder.
Configuration
Permission configuration can be found in the folders menu of the frontend.
Compilation album
If a song has compilation tag, its album will be added to the list of albums of each artist in its artists tag (not to be confused with album artists). For example, if a song has album named "album", compilation enabled, 2 artists "artist1", "artist2" and 1 album aritst "various artists", all of these 3 artists will have album "album" in their information. However, when accessing by album id, only album artists ("various artists" in this case) will be shown in the aritst fields.
Roadmap
- More compatible with Opensubsonic API.
- Fully-feature frontend.
- SQL playlist builder.
- Possibly integrating local machine learning model (like how Immich is doing with image/videos).