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.
Do you really think generic Linux phones will ever be a thing? The people go where the apps are and there's no reason for the most popular apps to make generic Linux apps. Just the idea of mobile Flatpaks makes me nauseous.