Navidrome Music Server (Unofficial)

288 readers
2 users here now

Navidrome is a free, open source web-based music collection server and streamer. It gives you freedom to listen to your music collection from any browser or mobile device. https://www.navidrome.org

This is an unofficial community. However, we adhear to the official Code Of Conduct set by the Navidrome project.

founded 1 year ago
MODERATORS
26
11
submitted 10 months ago* (last edited 9 months ago) by sabreW4K3@lemmy.tf to c/navidrome@discuss.tchncs.de
 
 

What?

Hello everyone, you may know me from films such as... Kidding. But hello nonetheless, today I'm releasing the culmination of all of my recent sleepless nights.

This simple script scans the location of your Navidrome music collection, pulls the embedded rating and then adds that rating to your Navidrome account

Code

v 0.3: Not only was the last version hideous, it was inconsistent. Stuff that worked in single directory mode never worked in full mode. Also removed some print statements, cleaned up some others.

import os
import mutagen
import requests
import urllib.parse
import json
import sys
import glob
from mutagen.id3 import ID3
global rating
global track_id

# Navidrome credentials
script_name = "ImportClementineRatings"
navidrome_url = "your-navidrome-server:port"
navidrome_username = "your-navidrome-username"
navidrome_password = "your-navidrome-password"
headers = None

# Directory containing MP3 files
mp3_directory = "your-collection-relative-to-this-script"

# Single Directory Mode
if len(sys.argv) > 1:
  for arg in sys.argv:
    #print(arg)
    #if arg != "import_ratings.py":
    if arg != os.path.basename(__file__):
      mp3_directory = "/".join([mp3_directory,"Collection",arg])

def extract_rating(mp3_file):
    audio = mutagen.File(mp3_file)
    tags = ID3(mp3_file)
    if "TXXX:FMPS_Rating_Amarok_Score" in tags:
      rating = tags["TXXX:FMPS_Rating_Amarok_Score"]
    else:
      print(" ".join(["No rating exists for",mp3_file,"this song"]))
      rating = None

    if (rating != None):
      sanerating = float(str(rating))
    else:
      sanerating = float(0)

    if sanerating >= 1.0:
      return 5
    elif sanerating >= 0.8:
      return 4
    elif sanerating >= 0.6:
      return 3
    elif sanerating >= 0.4:
      return 2
    elif sanerating >= 0.2:
      return 1
    else:
      return 0

def update_rating_on_navidrome(track_id, rating):
    hex_encoded_pass = navidrome_password.encode().hex()
    #print(rating)
    if rating != 0:
      url = f"{navidrome_url}/rest/setRating?id={track_id}&u={navidrome_username}&p=enc:{hex_encoded_pass}&v=1.12.0&rating={rating}&c={script_name}"
      response = requests.get(url)
      print(f"Success!")

def find_track_id_on_navidrome(mp3_file):
    track_id = None

    # Remove File Extension
    song = mp3_file.rsplit(".",1)[0]

    # Fetch Song Artist From Filename
    songartist = song.split(" - ")[0]
    songartist = songartist.split("/")[-1]

    # Fetch Song Title From Filename
    index_var = 1
    if 0 <= index_var < len(song.split(" - ")):
      songtitle = song.split(" - ")[1]
    else:
      return None
      
    #songtitle = urllib.parse.quote(songtitle)
    hex_encoded_pass = navidrome_password.encode().hex()

    if len(songtitle) < 2:
      return None
    else:
      #print(songtitle)
      songtitle = song.split(" - ")[1]
      songtitle = urllib.parse.quote(songtitle)

    url = f"{navidrome_url}/rest/search3?query={songtitle}&u={navidrome_username}&p=enc:{hex_encoded_pass}&v=1.12.0&c={script_name}&f=json"
    data = None

    response = requests.get(url)
    parsed = json.loads(response.content)
    print(f"Debug URL: {url}")
    if "subsonic-response" in parsed:
      parsed = parsed["subsonic-response"]
      if "searchResult3" in parsed:
        parsed = parsed["searchResult3"]
        if "song" in parsed:
           for match in parsed["song"]:
             special_characters = ":?*"
             if any(character in special_characters for character in  match["artist"]):
                match["artist"] = match["artist"].translate({ord(c): "_" for c in special_characters})

             if (match["artist"] == songartist):
               parsed = match
               track_id = match["id"]

    songtitle = urllib.parse.unquote(songtitle)
    if response.status_code == 200:
        if track_id:
          print(f"Track successfully identified: {songtitle}: {track_id}")
          return track_id
        else:
          print(f"Could not find {songtitle} track") 
          return None
    else:
        print(f"Failed to identify track {songtitle}: {response.text}")
        return None

def process_file(mp3_file, folder):
  track_id = "fail"

  mp3_file = "/".join([folder, mp3_file])
  rating = extract_rating(mp3_file)
  track_id = find_track_id_on_navidrome(mp3_file)

  if track_id != "fail":
    try:
      update_rating_on_navidrome(track_id, rating)
    except:
      print(f"Failed to set rating for {file}")

notmusicext = ("DS_Store","jpg", ".JPG", ".jpeg", ".JPEG", ".mood", ".m3u", ".nfo", ".png", ".PNG", ".sfv", ".url")

for foldername in os.listdir(mp3_directory):
  if foldername.endswith(".mp3"):
    process_file(foldername, mp3_directory)
    folderpath = mp3_directory
  elif foldername.endswith(notmusicext):
    print(f"Skipping: {foldername}")
  else:
    folderpath = "/".join([mp3_directory, foldername])
  #for filename in glob.iglob(mp3_directory + "*.mp3", recursive=True):
  #can't get this to work
    #would do stuff here
    print(f" Debug: folderpath")

    for filename in os.listdir(folderpath):
      if filename.endswith(".mp3"):
        process_file(filename, folderpath)
      elif filename.endswith(notmusicext):
        print(f"Skipping: {filename}")
      else:
        foldername2 = "/".join([folderpath,filename])
        for filename2 in os.listdir(foldername2):
          if filename2.endswith(".mp3"):
            if filename2.startswith("A") == False:
              process_file(filename2, foldername2)
          elif filename2.endswith(notmusicext):
            print(f"Skipping: {filename2}")
          else:
            print(f"What is: {filename2}")

print("Done!")

Usage

Copy the code block, create a Python script in your chosen directory and then run it.

Thanks

This truly would not be possible without the Fediverse community, especially the Lemmy community. So thank you everyone but especially

@Deebster@programming.dev @ggwithgg@feddit.nl @jnovinger@programming.dev @ishanpage@programming.dev @rglullis@communick.news @oscar@programming.dev @TootSweet@lemmy.world @sabret00the@mas.to @amcewen@mastodon.me.uk @oblomov@sociale.network @mdylanbell@fosstodon.org @mborous@mastodon.social @cohomologyisFUN@mastodon.sdf.org @eichin@mastodon.mit.edu @mdione@en.osm.town

For some of you what you did seemed so small, but it was massive to me and I'm incredibly grateful for your kindness.

The Lore

One day, one man decided that enough was enough, it was time to move to the modern age, but to get there he needed his music collection. However it wasn't enough to just have the music, he needed the ratings too. First world problems, I know! So with nothing but hope, he said out in search of the ring! I mean the script! He couldn't find the script, but Deebster offered to help and so the two of them embarked on a journey whereby I kept pasting stuff to Desbster and he was like "no" but under his guidance, my script born from the embers of Bard, started taking shape. Now, for me with zero Python experience, you can imagine that the guidance I required was a lot, but Deebster earned his Scout badge in guidance. And as I got closer and closer, I kept cutting my sleep short so that I could spend some extra time trying to figure it out. Also huge thanks to Minz from the Navidrome discord as they came in clutch with some API advice. Anyway, I got a working script and I'm paying it forward by sharing it. Thank you all again.

Anything Else

If you can see how I should improve this, please let me know. Thank you all again.

27
 
 

When I enter the directory creator command it says ''No such file or directory'' What did I miss?

28
 
 

I know of a MusicBee plugin, but don't know of a method that doesn't involve Windows. Does anyone perhaps know if one?

29
 
 

Crosspost from !selfhosted@lemmy.world

30
3
submitted 10 months ago* (last edited 10 months ago) by sabreW4K3@lemmy.tf to c/navidrome@discuss.tchncs.de
 
 

I'm curious if there's a specific way to go about doing this?

Can I just set my docker-compose.yaml to

volumes:
      - "/opt/navidrome/data:/data"
      - "http://192.168.0.88/Shared Music/:/music:ro"

Or do I have to mount the directory first

Or can I use the ND_BASEURL?

31
16
submitted 10 months ago* (last edited 10 months ago) by ryan_harg@discuss.tchncs.de to c/navidrome@discuss.tchncs.de
 
 

Another bug fix point release of navidrome is available.

32
 
 

Does anyone know how to auto-import playlists in #Navidrome ?

In my config I have set

MusicFolder = '/home/horn/Music'PlaylistsPath = './Playlist'AutoImportPlaylists = true

and in /home/horn/Music/Playlist/ there's a subfolder for each playlist containing FLAC files. For each playlist /home/horn/Music/Playlist/Foo/ there's also a /home/horn/Music/Playlist/Foo.m3u playlist file containing lines like /home/horn/Music/Playlist/Foo/FooArtist - FooSong.flac. However, those playlists don't show up in #Navidrome. What am I doing wrong?

@navidrome

33
 
 

I'm planning to put Navidrome on a Raspberry Pi in order to manage and consume my music collection, however I've realized that all my ratings are stored in-file and given that Navidrome is multi-user, I'm wondering how that works? Will I be able to preserve my ratings? Will Navidrome even pick them up?

34
12
submitted 11 months ago* (last edited 11 months ago) by ryan_harg@discuss.tchncs.de to c/navidrome@discuss.tchncs.de
 
 

I just realised that almost three weeks ago, a new minor navidrome version (0.50.1) has been released. Just wanted to mention it in case some of you haven't taken note as well. This release contains mostly bugfixes.

Most of all I'm happy to see some activity again. Hoping to see more in the near future!

35
 
 

I've made a Last.fm API key but at the point of adding it to Navidrome I wondered why I was bothering; is this option only there because of backwards compatibility or is there a tangible benefit?

36
 
 

Just setup navidrome, got my last.fm API keys

Running everything in docker. Music plays good on mobile and in the laptop browser.

In my environment variables I set : ND_LASTFM_APIKEY: xxxxx...

But scrobbling is not working, should the keys be key: value or key: "value"

PS. I setup the other 4 last.fm variables also

37
 
 

I'm using MusicBrainz Picard for tagging my music library and I'm not unhappy with it. Yet, sometimes it feels a bit clunky and I'm wondering if there are other good tools out there. What do people use? What are the major pros of these tools? Let's collect some recommendations here. I'm Linux based, but for the benefit of others don't restrict yourself!

38
39
 
 

Not strictly Navidrome news, but related. Version 3.5.0 of my favourite mobile subsonic client app was released. It delivers some new features (like filtering downloaded items by track, album or artist), several bugfixes and a full german localization.

You can download it from Github releases or IzzyOnDroid.

40
 
 

I've been using substreamer to access my Navidrome server from my android phone initially but I just came to learn about Tempo, which is also a nice app. What I like about substreamer among others is the option to let it download fanart from an external source by itself bypassing navidrome. There might be more nice apps, so I'm curious what you folks are using on your mobile devices.

41
1
submitted 1 year ago* (last edited 1 year ago) by ryan_harg@discuss.tchncs.de to c/navidrome@discuss.tchncs.de
 
 

I've noticed that I cannot play radio stations that I have added to Navidrome. The player just shows a cycling icon and the radio won't start playing. Looking at network activity, I could see the browser complaining about CORS issues: it cannot deliver content from a third-party domain. It turns out the issue is known and there should be a fix available in the next release: https://github.com/navidrome/navidrome/issues/2267 .