-
-
Notifications
You must be signed in to change notification settings - Fork 1
Rerelativate
Eric Mink edited this page Feb 12, 2022
·
1 revision
When I change my playlists on the phone, they will have full paths sometimes. Running python3 rerelativate.py
fixes this by turning them into relative paths once more.
Please read the code first. This is the script I am using for myself, not polished for the public. You will want to adapt a few paths.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright LucidBrot since 19.11.2020. Private use allowed.
import os, sys, shutil
import unicodedata
# The goal of this file is to
# * Tranform playlist files in m3u format that use absolute paths into relative paths
# * The paths should be equivalent if the absolute path was valid in the current system
# * Otherwise the first entry in PREFIXES that matches the absolute path is replaced with whatever it maps to
# Ideally only run it when no conflicting sync is scheduled by some other device. But since that is hard to know programmatically, this is not enforced.
# Known Issues:
# (None, Currently)
#
# Believed Fixed:
# 1. Sometimes it seems like I have entries in the playlists with ../../blah instead of ../blah. Maybe the script that updates
# the unassigned songs is broken?
# Fix: I don't think it has been broken in the past few months, but I don't recall whether/what I fixed.
#
# 2. Unicode symbols! Again. I think that may be a problem with my update script.
# Fix: yes, the problem was a weird unicode normalization that messed up my filenames on disk.
# This file is in a working state: TRUE
PHONE_SDCARD = "6F63-1277"
DESKTOP_PC_PATH = r"N:\\Files\Musik"+"\\"
DESKTOP_PC_PATH_TWO = "N:\\Files\\Musik\\"
DESKTOP_CYGWIN_PATH = "/cygdrive/n/Files/Musik/"
MUSIC_ROOT_RELATIVE_TO_PLAYLIST_LOCATION = "../"
PREFIXES = {
PHONE_SDCARD+"/Music/" : MUSIC_ROOT_RELATIVE_TO_PLAYLIST_LOCATION,
"/"+PHONE_SDCARD+"/Music/" : MUSIC_ROOT_RELATIVE_TO_PLAYLIST_LOCATION,
DESKTOP_PC_PATH : MUSIC_ROOT_RELATIVE_TO_PLAYLIST_LOCATION,
DESKTOP_PC_PATH_TWO : MUSIC_ROOT_RELATIVE_TO_PLAYLIST_LOCATION,
DESKTOP_CYGWIN_PATH : MUSIC_ROOT_RELATIVE_TO_PLAYLIST_LOCATION,
}
RELATIVE_PLAYLISTS_DIR = "../playlists_relative"
PYTHONFILE_DIR = os.path.dirname(__file__)
PLAYLISTS_DIR_PATH = os.path.abspath(os.path.join(PYTHONFILE_DIR, RELATIVE_PLAYLISTS_DIR))
def rerelativate (playlist_file_path):
""" rerelativates the file, overwriting it. A backup file is created and will be silently overwritten
next time."""
tmpfile = playlist_file_path.rstrip('/')+'.tmp'
bakfile = playlist_file_path.rstrip('/')+'.bak'
shutil.copyfile(playlist_file_path, bakfile)
encodings_to_try = ['UTF-8']
encoding_ctr = -1
# TODO: try with tokenize open instead? http://python-notes.curiousefficiency.org/en/latest/python3/text_file_processing.html#the-binary-option
# TODO: or can I read it in binary and use the path?
while (encoding_ctr < len(encodings_to_try)-1):
encoding_ctr += 1
try:
with open(playlist_file_path, 'r', encoding=encodings_to_try[encoding_ctr]) as fi, \
open(tmpfile, 'w', encoding=encodings_to_try[encoding_ctr]) as fo:
for entry in fi:
# remove newlines
entry = entry.replace('\r', '').replace('\n', '')
# disregard comments
if entry.startswith('#'):
fo.write(entry+'\n')
continue
# normalize unicode
# https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize
# disabled because I don't understand it and am not sure it's helping or hurting
# I think it would only help if it were always applied. But otherwise it falsifies the paths.
#if (encodings_to_try[encoding_ctr] == 'UTF-8'):
# entry = unicodedata.normalize('NFC', entry)
if os.path.isabs(entry):
entry_path_correct = entry
else:
# entry is relative to the playlist file, not relative to the current script path
entry_path_correct = os.path.join(os.path.dirname(playlist_file_path), entry)
# if a path is valid absolute path on the current system, turn it into a valid relative path
# if a path is not absolute but valid on the current system, that's fine as well
if ( os.path.isfile(entry_path_correct) ):
fo.write(os.path.relpath(entry_path_correct, start=os.path.dirname(playlist_file_path))+'\n')
continue
else:
pass
# this in itself is not that important because despite that being printet, all might be going well.
#print("[ debugtag1 ]: {} does not exist.".format(entry_path_correct), file=sys.stderr)
# otherwise the path is not valid on the current system or the target file is not existent.
# In that case, we work with the original entry, not the entry_path_correct.
# Match prefixes.
written = False
for (prefix, replacement) in PREFIXES.items():
if entry.startswith(prefix):
new_entry = replacement + entry[len(prefix):]
fo.write(new_entry+'\n')
written = True
pathpath = os.path.normpath(os.path.join(os.path.dirname(playlist_file_path), new_entry))
if not ( os.path.isfile(pathpath) ):
print("[rerelativate_entries]: Music File does not exist!\n"+
" {}\n".format(new_entry)+
" {}\n".format(pathpath)+
" encountered in playlist {}\n".format(playlist_file_path), file=sys.stderr)
break
if not written:
# It's likely that something is wrong with either the playlist entry or my music library
# Maybe the song is actually not where the entry says?
# It it says it should be in the same directory as the playlist file, maybe it means it should be in the music root?
dirpath_of_entry, filename_of_entry = os.path.split(os.path.relpath(entry_path_correct, start=os.path.dirname(playlist_file_path)))
if dirpath_of_entry is None or dirpath_of_entry == '':
wild_guess = os.path.join(os.path.dirname(playlist_file_path), MUSIC_ROOT_RELATIVE_TO_PLAYLIST_LOCATION, filename_of_entry)
if os.path.isfile(wild_guess):
fo.write(wild_guess+'\n')
written = True
if not written:
# Similarly, maybe it's relative to the path one too high.
# Entry: ../../yey.mp3 Should Be: ../yey.mp3
if entry.startswith("../"):
maybeentry = entry[3:]
if os.path.isfile(os.path.relpath(maybeentry, start=os.path.dirname(playlist_file_path))):
fo.write(maybeentry+'\n')
written=True
if not written:
print("[rerelativate_entries]: Not sure what to do with this entry! Leaving it untouched.\n"+
" encountered in playlist {}\n".format(playlist_file_path)+
" {}\n".format(entry), file=sys.stderr)
fo.write(entry+'\n')
# clean up
os.remove(playlist_file_path)
os.rename(tmpfile, playlist_file_path)
except OSError:
print( "[ rerelativate ]: Cannot open Playlist File!\n"+
" {}\n".format(playlist_file_path), file=sys.stderr)
except UnicodeDecodeError as e:
print( "[ rerelativate ]: Unicode Encoding issues with file!\n"+
" {}\n".format(playlist_file_path), file=sys.stderr)
print(e, file=sys.stderr)
print("will retry: {}".format('yes' if encoding_ctr < len(encodings_to_try)-1 else 'no'), file=sys.stderr)
def rerelativate_all_in ( dir_with_playlists ):
playlist_files = [os.path.join(dirpath, filnam) for (dirpath, _dirs, filpaths) in os.walk(dir_with_playlists) for filnam in filpaths if ( filnam.endswith('.m3u') or filnam.endswith('.m3u8') )]
for playlist_file in playlist_files:
#input("considering playlist file {}\nexists: {}".format(os.path.normpath(playlist_file), os.path.isfile(playlist_file)))
rerelativate(playlist_file)
def safety(playlistdir=PLAYLISTS_DIR_PATH):
print("---------------------------------------------")
print(" -- R e R e l a t i v a t e -- ")
print(" || || ")
print(" -- Eric Mink / LucidBrot 2020 -- ")
print("---------------------------------------------")
print("")
print("This run will OVERWRITE every m3u/m3u8 file in the directory at {}".format(playlistdir))
print("It should make backups as .bak though.")
print("Note that we're expecting UTF-8 files even in the .m3u case")
#print(" [debug]: isdir = {}".format(os.path.isdir(playlistdir)))
print("Press ENTER to proceed or abort with CTRL+C")
input()
def main(playlistdir=PLAYLISTS_DIR_PATH):
safety(playlistdir)
rerelativate_all_in(playlistdir)
if __name__ == "__main__":
main()