import os import getopt import sys import pathlib import mobject total_proc = 0 total_succ = 0 total_error = 0 total_cover_exists = 0 total_cover_written = 0 cover_missing = [] errors = [] input_dir = None output_dir = None cover_dir = None overwrite_cover = False cover_arts = [] WILDCARD_MATCH_SCORE = 1 ARTIST_MATCH_SCORE = 10 ALBUM_MATCH_SCORE = 100 TITLE_MATCH_SCORE = 1000 escape_list : list[tuple[str, str]] = [ ("%..%" , ":"), ("%sl%" , "/") ] def escape_str(input : str) -> str: for tup in escape_list: input = input.replace(tup[0], tup[1]) return input # to get a > 0 score, each field either must match or the cover tuple is wildcare (None) def calc_cover_score(artist: list[str], album: list[str], title: list[str], tup : tuple) -> int: score = 0 if (tup[0] == artist): score += ARTIST_MATCH_SCORE elif (tup[0] == [""]): score += WILDCARD_MATCH_SCORE else: # mismatched artist, discard return 0 if (tup[1] == album): score += ALBUM_MATCH_SCORE elif (tup[1] == [""]): score += WILDCARD_MATCH_SCORE else: # mismatched album, discard return 0 if (tup[2] == title): score += TITLE_MATCH_SCORE elif (tup[2] == [""]): score += WILDCARD_MATCH_SCORE else: # mismatched title, discard return 0 return score # returns the idx into cover_arts array. # < 0 means no match def find_best_cover(artist: list[str], album: list[str], title: list[str]) -> int: best_idx = -1 best_score = 0 for i in range(0, len(cover_arts)): score = calc_cover_score(artist, album, title, cover_arts[i]) if score > best_score: best_idx = i best_score = score return best_idx def plus_str_to_list(s : str) -> list[str]: ss : list[str] = s.split("+") ret = [] for sss in ss: ret.append(sss) return ret def parse_triple(f: str, escape : bool = True) -> tuple[list[str], list[str], list[str]]: triple = os.path.splitext(os.path.basename(f))[0].split("_") album = "" title = "" artist = plus_str_to_list(escape_str(triple[0]) if escape else triple[0]) if len(triple) >= 2: album = plus_str_to_list(escape_str(triple[1]) if escape else triple[1]) if len(triple) >= 3: title = plus_str_to_list(escape_str(triple[2]) if escape else triple[2]) return (artist, album, title) def load_cover_arts(): global cover_arts if cover_dir == None: return cdir = cover_dir files = os.listdir(cdir) for f in files: try: artist, album, title = parse_triple(f) f = os.path.join(cover_dir, f) print("Processing cover art - " + f) fp = pathlib.Path(f) if (fp.suffix.lower() == ".png"): type = "PNG" else: type = "JPEG" with open(f,'rb') as img: cover_arts.append((artist, album, title, img.read(), type, f)) print(" Loading cover art - " + f + " Artist: " + str(artist) + " Album: " + str(album) + " Title: " + str(title) + " Type: " + type) except Exception as e: print(" Skipping due to Exception - " + str(e)) print("") def usage(): print("Usage: python musik.py [options]\n\n\ options:\n\ -h : show usage.\n\ -i input : input directory\n\ -o output : output directory\n\ -c cover : cover arts page\n") def process_file(f: str, odir: str): global total_cover_exists global total_cover_written global total_succ global total_error global total_proc try: print("Processing file - " + f) total_proc += 1 artist, album, title = parse_triple(f) fartist, falbum, _ = parse_triple(f, escape=False) if len(title) == 0: raise Exception("No title") mobj = mobject.create_mobject(f) print(f" Artist: \"{mobj.get_artist()}\" -> \"{artist}\"\n" + f" Album: \"{mobj.get_album()}\" -> \"{album}\"\n" + f" Title: \"{mobj.get_title()}\" -> \"{title}\"") mobj.set_artist(artist) mobj.set_album_artist(artist) mobj.set_album(album) mobj.set_title(title) if (mobj.get_cover() == None or overwrite_cover): cover_idx = find_best_cover(artist, album, title) if (cover_idx >= 0): mobj.set_cover(cover_arts[cover_idx][3], cover_arts[cover_idx][4] == "PNG") print(f" Cover: <{'None' if mobj.get_cover() == None else 'Exists'}> -> \"{cover_arts[cover_idx][5]}\"") total_cover_written += 1 else: print(f" Cover: ") cover_missing.append(f) else: print(" Cover: ") total_cover_exists += 1 target_dir = os.path.join(odir, "+".join(fartist), "+".join(falbum)) os.makedirs(target_dir, exist_ok = True) target_f = os.path.join(target_dir, os.path.basename(f)) with open(target_f, "wb") as tf: tf.write(mobj.get_bytes()) print(" Written file to: " + target_f) total_succ += 1 except Exception as e: total_error += 1 errors.append(f) print(" Skipping due to exception - %s" % str(e)) print("") def process_directory(idir: str, odir: str): print("Processing directory - " + idir) files = os.listdir(idir) for f in files: f = os.path.join(idir, f) if os.path.isdir(f): process_directory(f, odir) else: process_file(f, odir) def main(): global input_dir global output_dir global cover_dir global overwrite_cover try: opts , _ = getopt.getopt(sys.argv[1:], "hi:o:c:f") except getopt.GetoptError as err: print(str(err)) usage() sys.exit(1) for o, a in opts: if o == "-h": usage() sys.exit(0) elif o == "-i": input_dir = a elif o == "-o": output_dir = a elif o == "-c": cover_dir = a elif o == "-f": overwrite_cover = True else: print("Unrecognized option: " + o) sys.exit(1) if input_dir == None or output_dir == None: print("Must specify both -i and -o") sys.exit(1) load_cover_arts() process_directory(input_dir, output_dir) print("\nSummary - Files: " + str(total_proc) + ". Processed: " + str(total_succ) + ". Cover Exists: " + str(total_cover_exists) + ". Cover Written: " + str(total_cover_written)) if (len(errors) > 0): print(f"Errors: {len(errors)}") for f in errors: print(f" {f}") if (len(cover_missing) > 0): print(f"Cover Missing: {len(cover_missing)}") for f in cover_missing: print(f" {f}") main()