-
-
Save bagaag/3d64e3349b6ed3bfd6e01813222db055 to your computer and use it in GitHub Desktop.
# NOTE: Navidrome 0.55.0 now supports moved and renamed files in the library during scan, so this | |
# script is no longer needed. | |
# This script changes the path for a folder or file in Navidrome's database, allowing music files to be | |
# moved or renamed on the file system without losing associated metadata in Navidrome. Since the original | |
# version, it has been updatd to account for the media_file IDs, which are calculated from the path value | |
# and referenced in several tables. | |
# | |
# This script is based on Navidrome version 0.49.2. If you are running an older version of Navidrom, it | |
# will likely fail. If you are running a newer version of Navidrome, your mileage may vary. | |
# | |
# It does NOT make any modifications to the file system - only to the Navidrome database. | |
# | |
# It does not rescan the file; it assumes nothing has changed but the path. If you're moving files | |
# and also updating their contents (e.g. tags or bitrate), run this to change the path(s) in the | |
# database, and then run a full scan to update the metadata. | |
# | |
# Place this file in the same directory as navidrome.db, which is /var/lib/navidrome on Linux, and be sure | |
# to use fully qualified paths for arguments. It must be run as a user that has write access to the | |
# navidrome.db file. | |
# | |
# Generic use - note that you may need to use python3 instead of python, depending on your system: | |
# python change_path.py FROM_PATH TO_PATH | |
# | |
# Example: Rename/move a folder (note trailing slashes): | |
# python change_path.py /mnt/music/artists/Bjork/ /mnt/music/artists/Björk/ | |
# | |
# Example: Rename a song file (use quotes for paths with spaces): | |
# python change_path.py "/mnt/music/artists/Test 1/song.mp3" "/mnt/music/artists/Test 2/01 - Song.mp3" | |
# | |
# The script's output lists each path updated along with the MD5 ID calculated from it and the row | |
# count updates for each table referencing the old ID. | |
# | |
# Note that Navidrome's scanner will automatically remove files from its database if they're found to | |
# be missing, so it's important to stop the Navidrome server process before moving files, and to update | |
# the database with this script prior to restarting it. | |
# | |
# Steps to use: | |
# 1. Stop the Navidrome server. | |
# 2. Make a backup of your navidrome.db file so you can roll back any unwanted changes. | |
# 3. Move or rename folders and files as needed on the file system. | |
# 4. Run this script to update the Navidrome database for any files/folders moved or renamed. | |
# 5. Start Navidrome service. | |
# 6. Optionally run a full scan if file contents have changed. | |
# | |
# Source: https://gist.github.com/bagaag/3d64e3349b6ed3bfd6e01813222db055 | |
# | |
import sqlite3 | |
import hashlib | |
import os | |
import sys | |
def path_clause(path): | |
if path.endswith('/'): | |
return ('LIKE', '%') | |
else: | |
return ('=', '') | |
def get_matching_media(path): | |
clause_and_suffix = path_clause(path) | |
clause = clause_and_suffix[0] | |
path = path + clause_and_suffix[1] | |
sql = f""" | |
SELECT id, path FROM media_file | |
WHERE path {clause} ? | |
ORDER BY path;""" | |
cur = con.cursor() | |
res = cur.execute(sql, [path]) | |
ret = res.fetchall() | |
cur.close() | |
return ret | |
def exec_update(sql, params): | |
cur = con.cursor() | |
cur.execute(sql, params) | |
con.commit() | |
rc = cur.rowcount | |
cur.close() | |
return rc | |
def replace_values(res): | |
replaced = [] | |
for row in res: | |
old_id = row[0] | |
old_path = row[1] | |
new_path = to_path | |
if from_path.endswith('/'): | |
new_path = old_path.replace(from_path, to_path) | |
else: | |
new_path = to_path | |
new_id = md5(new_path) | |
sql = f""" | |
UPDATE media_file | |
SET path = ?, id = ? | |
WHERE path = ? and id = ?; | |
""" | |
media_file = exec_update(sql, (new_path, new_id, old_path, old_id)) | |
sql = f""" | |
UPDATE annotation | |
SET item_id = ? | |
WHERE item_id = ? | |
AND item_type='media_file'; | |
""" | |
annotation = exec_update(sql, (new_id, old_id)) | |
sql = f""" | |
UPDATE media_file_genres | |
SET media_file_id = ? | |
WHERE media_file_id = ?; | |
""" | |
media_file_genres = exec_update(sql, (new_id, old_id)) | |
sql = f""" | |
UPDATE playlist_tracks | |
SET media_file_id = ? | |
WHERE media_file_id = ?; | |
""" | |
playlist_tracks = exec_update(sql, (new_id, old_id)) | |
sql = f""" | |
UPDATE bookmark | |
SET item_id = ? | |
WHERE item_id = ? | |
AND item_type = 'media_file'; | |
""" | |
bookmark = exec_update(sql, (new_id, old_id)) | |
sql = f""" | |
UPDATE album | |
SET embed_art_path = ? | |
WHERE embed_art_path = ?; | |
""" | |
album = exec_update(sql, (new_path, old_path)) | |
replaced.append({ | |
'old_id': old_id, | |
'old_path': old_path, | |
'new_id': new_id, | |
'new_path': new_path, | |
'media_file': media_file, | |
'annotation': annotation, | |
'media_file_genres': media_file_genres, | |
'playlist_tracks': playlist_tracks, | |
'bookmark': bookmark, | |
'album': album | |
}) | |
return replaced | |
def md5(s): | |
return hashlib.md5(s.encode('utf-8')).hexdigest() | |
# | |
# main | |
# | |
if len(sys.argv) < 3: | |
print("Usage: python change_path.py FROM_PATH TO_PATH") | |
exit() | |
from_path = sys.argv[1] | |
to_path = sys.argv[2] | |
if (from_path.endswith(os.sep) and not to_path.endswith(os.sep)) or (not from_path.endswith(os.sep) and to_path.endswith(os.sep)): | |
print("One path has a trailing slash and the other doesn't. That's probably not right. Check your inputs.") | |
exit() | |
con = sqlite3.connect('navidrome.db') | |
res = get_matching_media(from_path) | |
print(f'Found {len(res)} path matches.') | |
updated = replace_values(res) | |
con.close() | |
for update in updated: | |
print('FROM: ' + update['old_path']) | |
print(' ' + update['old_id']) | |
print('TO: ' + update['new_path']) | |
print(' ' + update['new_id']) | |
print(' media_file: ' + str(update['media_file'])) | |
print(' annotation: ' + str(update['annotation'])) | |
print(' media_file_genres: ' + str(update['media_file_genres'])) | |
print(' playlist_tracks: ' + str(update['playlist_tracks'])) | |
print(' bookmark: ' + str(update['bookmark'])) | |
print(' album: ' + str(update['album'])) |
Hi, @bagaag
I've added comments to the file but here are the things I've changed:
- Moved
album
table's queries to a separate function- No changes to
media_file
table or anythingid
related
- No changes to
- Updates album table's
image_files
andpaths
columns too - In case user gives an invalid library path as
FROM_PATH
parameter, the script exits - If
TO_PATH
parameter differs from database's library path, it updateslibrary
table and all media references- otherwise no changes are made to library path so single album path changes should still work
At least one bad thing is that if TO_PATH
changes the library path, it updates all media references, but doesn't check if the given path is an album path, for example. I'm too tired at the moment to think further. :) I commented on the file, that an user confirmation in such case would be wiser than just blindly proceeding.
Edit. Also, I'm not that familiar with Python and it's been a while since I had my SQL course in uni. Please say, if there's something that's not generally recommended or otherwise hacky solution, thanks. :)
Oh, and one question came to my mind:
Is there a reason for checking trailing slashes from users parameters and not stripping them away? Library and album tables update fine when stripping the slashes.
Album table's paths
might have /library/artist/album/cd1/library/artist/album/cd2
strings where trailing slashes need to be stripped away.
Actually, without the trailing slashes in both parameter, media_file's path
becomes incorrect.
Edit. Okay, it's for renaming single songs it seems.
Navidrome v0.55.0 has big changes so this script needs updating once more. My changes are not relevant anymore. Actually, v0.55.0 introduces Persistent IDs
which should solve renaming and/or moving the files.
Persistent IDs: Tracks and albums now use persistent IDs (PIDs), ensuring stability in playlists, favorites, and external integrations, even if your files move or are renamed.
Yes, I came here to say that. Navidrome v0.55.0 largely removes the need for this script as IDs can be based on any combination of tags instead of using the path to form the ID. The scanner will pick up moved or renamed files based on their tag values.
The scanner will pick up moved or renamed files based on their tag values.
@bagaag do you know how smart the scanner is with changing file types/extensions? I'm trying to get navidrome to recognize an "upgraded" version of a file (original file is .m4a
but I have a .flac
that I'd rather use).
Sorry if this is not the place to ask, but I had been using your script for years until this new version of navidrome dropped
The scanner will pick up moved or renamed files based on their tag values.
@bagaag do you know how smart the scanner is with changing file types/extensions? I'm trying to get navidrome to recognize an "upgraded" version of a file (original file is
.m4a
but I have a.flac
that I'd rather use).Sorry if this is not the place to ask, but I had been using your script for years until this new version of navidrome dropped
Actually fixed my issue here: navidrome/navidrome#3957 (reply in thread)
I need to make few more changes first when I have the time. At the moment it's parsed together quite poorly, hahah. I'll send it you after the changes.
Thanks again!