From b5c792b8d80d40d5f453afef916eb1170e7152c4 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 24 Aug 2022 12:44:56 +0100 Subject: [PATCH] Lots of work on replace_files.py --- app/replace_files.py | 320 ++++++++++++++++++++++++------------------- 1 file changed, 180 insertions(+), 140 deletions(-) diff --git a/app/replace_files.py b/app/replace_files.py index 913cda8..a7977ee 100755 --- a/app/replace_files.py +++ b/app/replace_files.py @@ -3,19 +3,10 @@ # Script to replace existing files in parent directory. Typical usage: # the current directory contains a "better" version of the file than the # parent (eg, bettet bitrate). -# -# Actions: -# -# - check that the same filename is present in the parent directory -# - check that the artist and title tags are the same -# - append ".bak" to the version in the parent directory -# - move file to parent directory -# - normalise file -# - update duration, start_gap, fade_at, silence_at, mtime in database import glob import os -import pydymenu +import pydymenu # type: ignore import shutil import sys @@ -34,181 +25,230 @@ from sqlalchemy.exc import IntegrityError from typing import List # ###################### SETTINGS ######################### -process_multiple_matches = True -do_processing = False -process_no_matches = False +process_name_and_tags_matches = True +process_tag_matches = True +do_processing = True +process_no_matches = True + +source_dir = '/home/kae/music/Singles/tmp' +parent_dir = os.path.dirname(source_dir) # ######################################################### def insensitive_glob(pattern): + """Helper for case insensitive glob.glob()""" + def either(c): return '[%s%s]' % (c.lower(), c.upper()) if c.isalpha() else c return glob.glob(''.join(map(either, pattern))) -# Check file of same name exists in parent directory -source_dir = '/home/kae/music/Singles/tmp' # os.getcwd() -parent_dir = os.path.dirname(source_dir) -assert source_dir != parent_dir - name_and_tags: List[str] = [] -name_not_tags: List[str] = [] tags_not_name: List[str] = [] multiple_similar: List[str] = [] no_match: List[str] = [] possibles: List[str] = [] - -print(f"{source_dir=}, {parent_dir=}") +no_match: int = 0 def main(): + global no_match + + # We only want to run this against the production database because + # we will affect files in the common pool of tracks used by all + # databases if 'musicmuster_prod' not in os.environ.get('MM_DB'): response = input("Not on production database - c to continue: ") if response != "c": sys.exit(0) - tracks = os.listdir(parent_dir) - for fname in os.listdir(source_dir): - parent_file = os.path.join(parent_dir, fname) - new_file = os.path.join(source_dir, fname) - us = get_tags(new_file) - us_t = us['title'] - us_a = us['artist'] - bitrate = us['bitrate'] - if os.path.exists(parent_file): - # File exists, check tags - p = get_tags(parent_file) - p_t = p['title'] - p_a = p['artist'] - if ( - (str(p_t).lower() != str(us_t).lower()) or - (str(p_a).lower() != str(us_a).lower()) - ): - name_not_tags.append( - f" {fname=}, {p_t} → {us_t}, {p_a} → {us_a}") - process_track(new_file, parent_file, us_t, us_a, bitrate) + # Sanity check + assert source_dir != parent_dir + + # Scan parent directory + with Session() as session: + all_tracks = Tracks.get_all(session) + parent_tracks = [a for a in all_tracks if parent_dir in a.path] + parent_fnames = [os.path.basename(a.path) for a in parent_tracks] + # Create a dictionary of parent paths with their titles and + # artists + parents = {} + for t in parent_tracks: + parents[t.path] = {"title": t.title, "artist": t.artist} + titles_to_path = {} + artists_to_path = {} + for k, v in parents.items(): + try: + titles_to_path[v['title'].lower()] = k + artists_to_path[v['artist'].lower()] = k + except AttributeError: continue - name_and_tags.append(new_file) - process_track(new_file, parent_file, us_t, us_a, bitrate) - continue + + for new_fname in os.listdir(source_dir): + new_path = os.path.join(source_dir, new_fname) + new_tags = get_tags(new_path) + new_title = new_tags['title'] + new_artist = new_tags['artist'] + bitrate = new_tags['bitrate'] + + # If same filename exists in parent direcory, check tags + parent_path = os.path.join(parent_dir, new_fname) + if os.path.exists(parent_path): + parent_tags = get_tags(parent_path) + parent_title = parent_tags['title'] + parent_artist = parent_tags['artist'] + if ( + (str(parent_title).lower() == str(new_title).lower()) and + (str(parent_artist).lower() == str(new_artist).lower()) + ): + name_and_tags.append( + f" {new_fname=}, {parent_title} → {new_title}, " + f" {parent_artist} → {new_artist}" + ) + if process_name_and_tags_matches: + process_track(new_path, parent_path, new_title, + new_artist, bitrate) + continue + + # Check for matching tags although filename is different + if new_title.lower() in titles_to_path: + possible_path = titles_to_path[new_title.lower()] + if parents[possible_path]['artist'].lower() == new_artist.lower(): + # print( + # f"title={new_title}, artist={new_artist}:\n" + # f" {new_path} → {parent_path}" + # ) + tags_not_name.append( + f"title={new_title}, artist={new_artist}:\n" + f" {new_path} → {parent_path}" + ) + if process_tag_matches: + process_track(new_path, possible_path, new_title, + new_artist, bitrate) + continue + else: + no_match += 1 + + else: + no_match += 1 # Try to find a near match - stem = fname.split(".")[0] - matches = insensitive_glob(os.path.join(parent_dir, stem) + '*') - match_count = len(matches) - if match_count == 0: - if process_no_matches: - prompt = f"\n file={fname}\n title={us_t}\n artist={us_a}: " - # Use fzf to search - choice = pydymenu.fzf(tracks, prompt) - if choice: - old_file = os.path.join(parent_dir, choice[0]) - oldtags = get_tags(old_file) - old_title = oldtags['title'] - old_artist = oldtags['artist'] - print() - print(f" File name will change {choice[0]}") - print(f" → {fname}") - print() - print(f" Title tag will change {old_title}") - print(f" → {us_t}") - print() - print(f" Artist tag will change {old_artist}") - print(f" → {us_a}") - print() - data = input("Go ahead (y to accept)? ") - if data == "y": - process_track(new_file, old_file, us_t, us_a, bitrate) - continue - else: - no_match.append(f"{fname}, {us_t=}, {us_a=}") - continue - no_match.append(f"{fname}, {us_t=}, {us_a=}") - continue - else: - no_match.append(f"{fname}, {us_t=}, {us_a=}") - continue - if match_count > 1: - multiple_similar.append(fname + "\n " + "\n ".join(matches)) - if match_count <= 26 and process_multiple_matches: - print(f"\n file={fname}\n title={us_t}\n artist={us_a}\n") - d = {} - while True: - for i, match in enumerate(matches): - d[i] = match - for k, v in d.items(): - print(f"{k}: {v}") - data = input("pick one, return to quit: ") - if data == "": - break - try: - key = int(data) - except ValueError: - continue - if key in d: - dst = d[key] - process_track(new_file, dst, us_t, us_a, bitrate) - break - else: - continue - continue # from break after testing for "" in data - # One match, check tags - sim_name = matches[0] - p = get_tags(sim_name) - p_t = p['title'] - p_a = p['artist'] - if ( - (str(p_t).lower() != str(us_t).lower()) or - (str(p_a).lower() != str(us_a).lower()) - ): - possibles.append( - f"File: {os.path.basename(sim_name)} → {fname}" - f"\n {p_t} → {us_t}\n {p_a} → {us_a}" - ) - process_track(new_file, sim_name, us_t, us_a, bitrate) - continue - tags_not_name.append(f"Rename {os.path.basename(sim_name)} → {fname}") - process_track(new_file, sim_name, us_t, us_a, bitrate) + # stem = new_fname.split(".")[0] + # matches = insensitive_glob(os.path.join(parent_dir, stem) + '*') + # match_count = len(matches) + # if match_count == 0: + if process_no_matches: + prompt = f"\n file={new_fname}\n title={new_title}\n artist={new_artist}: " + # Use fzf to search + choice = pydymenu.fzf(parent_fnames, prompt) + if choice: + old_file = os.path.join(parent_dir, choice[0]) + oldtags = get_tags(old_file) + old_title = oldtags['title'] + old_artist = oldtags['artist'] + print() + print(f" File name will change {choice[0]}") + print(f" → {new_fname}") + print() + print(f" Title tag will change {old_title}") + print(f" → {new_title}") + print() + print(f" Artist tag will change {old_artist}") + print(f" → {new_artist}") + print() + data = input("Go ahead (y to accept)? ") + if data == "y": + process_track(new_path, old_file, new_title, new_artist, bitrate) + continue + if data == "q": + sys.exit(0) + else: + continue + # else: + # no_match.append(f"{new_fname}, {new_title=}, {new_artist=}") + # continue + + # if match_count > 1: + # multiple_similar.append(new_fname + "\n " + "\n ".join(matches)) + # if match_count <= 26 and process_multiple_matches: + # print(f"\n file={new_fname}\n title={new_title}\n artist={new_artist}\n") + # d = {} + # while True: + # for i, match in enumerate(matches): + # d[i] = match + # for k, v in d.items(): + # print(f"{k}: {v}") + # data = input("pick one, return to quit: ") + # if data == "": + # break + # try: + # key = int(data) + # except ValueError: + # continue + # if key in d: + # dst = d[key] + # process_track(new_path, dst, new_title, new_artist, bitrate) + # break + # else: + # continue + # continue # from break after testing for "" in data + # # One match, check tags + # sim_name = matches[0] + # p = get_tags(sim_name) + # parent_title = p['title'] + # parent_artist = p['artist'] + # if ( + # (str(parent_title).lower() != str(new_title).lower()) or + # (str(parent_artist).lower() != str(new_artist).lower()) + # ): + # possibles.append( + # f"File: {os.path.basename(sim_name)} → {new_fname}" + # f"\n {parent_title} → {new_title}\n {parent_artist} → {new_artist}" + # ) + # process_track(new_path, sim_name, new_title, new_artist, bitrate) + # continue + # tags_not_name.append(f"Rename {os.path.basename(sim_name)} → {new_fname}") + # process_track(new_path, sim_name, new_title, new_artist, bitrate) print(f"Name and tags match ({len(name_and_tags)}):") # print(" \n".join(name_and_tags)) # print() - print(f"Name but not tags match ({len(name_not_tags)}):") - print(" \n".join(name_not_tags)) - print() + # print(f"Name but not tags match ({len(name_not_tags)}):") + # print(" \n".join(name_not_tags)) + # print() print(f"Tags but not name match ({len(tags_not_name)}):") # print(" \n".join(tags_not_name)) # print() - print(f"Multiple similar names ({len(multiple_similar)}):") - print(" \n".join(multiple_similar)) - print() + # print(f"Multiple similar names ({len(multiple_similar)}):") + # print(" \n".join(multiple_similar)) + # print() - print(f"Possibles: ({len(possibles)}):") - print(" \n".join(possibles)) - print() + # print(f"Possibles: ({len(possibles)}):") + # print(" \n".join(possibles)) + # print() - print(f"No match ({len(no_match)}):") - print(" \n".join(no_match)) - print() + # print(f"No match ({len(no_match)}):") + # print(" \n".join(no_match)) + # print() - print(f"Name and tags match ({len(name_and_tags)}):") - print(f"Name but not tags match ({len(name_not_tags)}):") - print(f"Tags but not name match ({len(tags_not_name)}):") - print(f"Multiple similar names ({len(multiple_similar)}):") - print(f"Possibles: ({len(possibles)}):") - print(f"No match ({len(no_match)}):") + # print(f"Name and tags match ({len(name_and_tags)}):") + # print(f"Name but not tags match ({len(name_not_tags)}):") + # print(f"Tags but not name match ({len(tags_not_name)}):") + # print(f"Multiple similar names ({len(multiple_similar)}):") + # print(f"Possibles: ({len(possibles)}):") + # print(f"No match ({len(no_match)}):") + print(f"No matches: {no_match}") def process_track(src, dst, title, artist, bitrate): new_path = os.path.join(os.path.dirname(dst), os.path.basename(src)) print( - f"process_track:\n {src=}\n {new_path=}\n " - f"{dst=}\n {title=}, {artist=}\n" + f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n" ) if not do_processing: