From d06cff4cbd0349e9a85c3829bcc40f013d9c60fa Mon Sep 17 00:00:00 2001 From: Lucas Vivo Date: Mon, 12 Oct 2026 16:33:52 +0200 Subject: [PATCH] compare playlists m3u und navidrome --- media handling/compare_playlists.py | 170 ++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100755 media handling/compare_playlists.py diff --git a/media handling/compare_playlists.py b/media handling/compare_playlists.py new file mode 100755 index 0000000..4d4a424 --- /dev/null +++ b/media handling/compare_playlists.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +import re +import sys +from pathlib import Path +from difflib import SequenceMatcher + +def normalize(name): + name = Path(name).stem.lower() + + # remove brackets + name = re.sub(r"\(.*?\)|\[.*?\]", "", name) + + # replace punctuation with spaces + name = re.sub(r"[^a-z0-9]", " ", name) + + # collapse whitespace + name = " ".join(name.split()) + + return name + + +def similarity(a, b): + words_a = set(normalize(a).split()) + words_b = set(normalize(b).split()) + + if not words_a or not words_b: + return 0.0 + + # Jaccard similarity + intersection = words_a & words_b + union = words_a | words_b + + return len(intersection) / len(union) + + +def load_playlist(path): + entries = [] + + with open(path, encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.strip() + + if not line or line.startswith("#"): + continue + + entries.append(Path(line).name) + + return entries + + +def compare_playlists(orig_file, new_file, threshold=0.85): + orig_files = load_playlist(orig_file) + new_files = load_playlist(new_file) + + print(f"\n=== {orig_file.parent} ===") + print(f"Comparing:") + print(f" {orig_file.name}") + print(f" {new_file.name}") + + missing = [] + + for orig in orig_files: + best_match = None + best_score = 0 + + for candidate in new_files: + score = similarity(orig, candidate) + + if score > best_score: + best_score = score + best_match = candidate + + if best_score >= threshold: + pass + # print( + # f"MATCH {best_score:.1%} " + # f"{orig} -> {best_match}" + # ) + else: + missing.append((orig, best_score, best_match)) + + if missing: + print("\nMISSING:") + for orig, score, best_match in sorted( + missing, + key=lambda x: x[1], + reverse=True, + ): + print( + f" {orig}" + f" (best={score:.1%}" + f"{', candidate=' + best_match if best_match else ''})" + ) + + return len(missing) + + +def compare_directory(root, threshold=0.85): + total_missing = 0 + playlist_pairs = 0 + + for orig_file in root.rglob("*_orig.m3u"): + new_file = orig_file.with_name( + orig_file.name.replace("_orig.m3u", ".m3u") + ) + + if not new_file.exists(): + print( + f"\nWARNING: Missing playlist for " + f"{orig_file}" + ) + continue + + playlist_pairs += 1 + total_missing += compare_playlists( + orig_file, + new_file, + threshold, + ) + + print("\n" + "=" * 80) + print(f"Playlists compared : {playlist_pairs}") + print(f"Total missing : {total_missing}") + + +def main(): + args = sys.argv[1:] + + threshold = 0.85 + + if len(args) == 3 and args[0] == "--threshold": + threshold = float(args[1]) + args = [args[2]] + + elif len(args) == 4 and args[0] == "--threshold": + threshold = float(args[1]) + args = [args[2], args[3]] + + if len(args) == 1: + path = Path(args[0]) + + if not path.is_dir(): + print("Single argument must be a directory") + sys.exit(1) + + compare_directory(path, threshold) + + elif len(args) == 2: + compare_playlists( + Path(args[0]), + Path(args[1]), + threshold, + ) + + else: + print( + f"Usage:\n" + f" {Path(sys.argv[0]).name} " + f" \n\n" + f" {Path(sys.argv[0]).name} " + f"\n\n" + f"Optional:\n" + f" --threshold 0.90" + ) + sys.exit(1) + + +if __name__ == "__main__": + main()