170 lines
3.8 KiB
Python
170 lines
3.8 KiB
Python
#!/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"<playlist_orig.m3u> <playlist.m3u>\n\n"
|
||
|
|
f" {Path(sys.argv[0]).name} "
|
||
|
|
f"<directory>\n\n"
|
||
|
|
f"Optional:\n"
|
||
|
|
f" --threshold 0.90"
|
||
|
|
)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|