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

@[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected] @[email protected]

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.

  • Deebster
    link
    English
    35 months ago

    Congratulations on finishing! And I’m glad you’re sharing this for the next person who wants to do similar.

    I see you’re doing a listdir on a listdir - did my for filename in glob.iglob(mp3_directory + '/**/*.mp3', recursive=True): suggestion not work for you?

    btw, I didn’t get a notification from the mention, I wonder if you added too many and it ignored them as an anti-spam thing?

    • @[email protected]OP
      link
      fedilink
      English
      25 months ago

      I had problems because my mp3 directory isn’t set up properly. Before I moved everything over, I had some files in a sleepers folder and another bunch in artist folders. But now that I’m getting everything in place properly, I should switch to your more sane method.

      And whaaaa? Bummer! I have no idea why. Maybe a bug with 0.19.x?

  • RyanM
    link
    fedilink
    3
    edit-2
    5 months ago

    Congratulations! Especially with zero experience, this is a big achievement, even with help! Thanks for giving the result back to the community!

  • @[email protected]OP
    link
    fedilink
    English
    1
    edit-2
    5 months ago

    Archive

    v0.1: Initial release was tested with a few directories and quickly had issues when I tried it against my real thing. I quickly ended up cursing Big Python and then decided to fix the issues.

    import os
    import mutagen
    import requests
    import urllib.parse
    import json
    from mutagen.easyid3 import EasyID3
    from mutagen.id3 import ID3
    from pprint import pprint
    global rating
    global track_id
    
    # Navidrome credentials
    script_name = "ImportClementineRatings"
    navidrome_url = "http://your-navidrome-server-location:4533"
    navidrome_username = "your-username"
    navidrome_password = "your-password"
    headers = None
    
    # Directory containing MP3 files
    mp3_directory = "the-location-of-your-music-directory-relative-to-where-this-script-will-be"
    
    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()
        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)
    
    def find_track_id_on_navidrome(mp3_file):
        # Remove File Extension
        song = mp3_file.rsplit(".",1)[0]
    
        # Fetch Song Artist From Filename
        songartist = song.split(" - ")[0]
        songtitle = song.split(" - ")[1]
    
        songtitle = urllib.parse.quote(songtitle)
        hex_encoded_pass = navidrome_password.encode().hex()
    
        if len(songtitle) < 2:
          return None
    
        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(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 a track") 
              return None
        else:
            print(f"Failed to identify track {songtitle}: {response.text}")
            return None
    
    for foldername in os.listdir(mp3_directory):
      folderpath = "/".join([mp3_directory, foldername])
      for filename in os.listdir(folderpath):
    
        if filename.endswith(".mp3"):
            mp3_file = "/".join([folderpath, filename])
            rating = extract_rating(mp3_file)
            print(filename)
            track_id = find_track_id_on_navidrome(filename)
    
            if track_id:
                update_rating_on_navidrome(track_id, rating)
    
    • @[email protected]OP
      link
      fedilink
      English
      1
      edit-2
      5 months ago

      v0.2: This version is a lot uglier, but allows you to feed a single directory to the command line and also doesn’t die when seeing common files in your music directories.

      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 = "navidrome-server:port"
      navidrome_username = "navidrome-username"
      navidrome_password = "navidrome-password"
      headers = None
      
      # Directory containing MP3 files
      mp3_directory = "relative-location-of-your-music-collection"
      
      # 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"Rating: {rating}")
      
      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
          songtitle = song.split(" - ")[1]
          songtitle = urllib.parse.quote(songtitle)
          hex_encoded_pass = navidrome_password.encode().hex()
      
          if len(songtitle) < 2:
            return None
      
          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(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)
        #print(f"Rating1: {rating}")
        #print(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}")
      
      for foldername in os.listdir(mp3_directory):
        if foldername.endswith(".mp3"):
          #process_file(foldername, mp3_directory)
          folderpath = mp3_directory
        elif foldername.endswith(".DS_Store"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".jpg"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".JPG"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".jpeg"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".JPEG"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".mood"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".m3u"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".nfo"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".png"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".PNG"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".sfv"):
          print(f"Skipping: {foldername}")
        elif foldername.endswith(".url"):
          print(f"Skipping: {foldername}")
        else:
          folderpath = "/".join([mp3_directory, foldername])
          print(folderpath)
      
          for filename in os.listdir(folderpath):
            if filename.endswith(".mp3"):
              process_file(filename, folderpath)
            elif filename.endswith(".DS_Store"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".jpg"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".JPG"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".jpeg"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".JPEG"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".mood"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".m3u"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".nfo"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".png"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".PNG"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".sfv"):
              print(f"Skipping: {filename}")
            elif filename.endswith(".url"):
              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(".DS_Store"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".jpg"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".JPG"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".jpeg"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".JPEG"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".mood"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".m3u"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".nfo"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".png"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".PNG"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".sfv"):
                  print(f"Skipping: {filename2}")
                elif filename2.endswith(".url"):
                  print(f"Skipping: {filename2}")
                else:
                  print(f"What is: {filename2}")
      print("Done!")