import xml.etree.ElementTree as ET import argparse import shutil from pathlib import Path def copy_hot_cues_to_memory_cues(xml_file_path, output_file_path=None, backup=True): """ Kopiert alle Hot Cues als Memory Cues in der Rekordbox XML. Args: xml_file_path (str): Pfad zur Rekordbox XML Datei output_file_path (str, optional): Ausgabedatei. Wenn None, wird die Original-Datei überschrieben backup (bool): Erstellt ein Backup der Original-Datei """ xml_path = Path(xml_file_path) if not xml_path.exists(): raise FileNotFoundError(f"XML-Datei nicht gefunden: {xml_file_path}") # Backup erstellen if backup: backup_path = xml_path.with_suffix(xml_path.suffix + '.backup') shutil.copy2(xml_path, backup_path) print(f"Backup erstellt: {backup_path}") # XML laden try: tree = ET.parse(xml_path) root = tree.getroot() except ET.ParseError as e: raise Exception(f"Fehler beim Parsen der XML-Datei: {e}") tracks_processed = 0 total_hot_cues_copied = 0 total_hot_cues_skipped = 0 tracks_skipped = 0 # Alle TRACK Elemente finden for track in root.findall(".//TRACK"): hot_cues_in_track = [] memory_cues_in_track = [] # Alle POSITION_MARK Elemente in diesem Track finden for position_mark in track.findall(".//POSITION_MARK"): num_attr = position_mark.get("Num") if num_attr: if num_attr.isdigit() and int(num_attr) >= 0: # Hot Cues haben Num >= 0 hot_cues_in_track.append(position_mark) elif num_attr == "-1": # Memory Cues haben Num = -1 memory_cues_in_track.append(position_mark) # Hot Cues als Memory Cues kopieren if hot_cues_in_track: # Prüfen, ob bereits mehr als 1 Memory Cue existiert if len(memory_cues_in_track) > 1: track_artist = track.get("Artist", "Unbekannt") track_title = track.get("Name", "Unbekannt") print(f"Track '{track_artist} - {track_title}' übersprungen - bereits {len(memory_cues_in_track)} Memory Cues vorhanden") tracks_skipped += 1 continue # Prüfen, ob nur ein Hot Cue vorhanden ist if len(hot_cues_in_track) == 1: track_artist = track.get("Artist", "Unbekannt") track_title = track.get("Name", "Unbekannt") print(f"Track '{track_artist} - {track_title}' übersprungen - nur 1 Hot Cue vorhanden") tracks_skipped += 1 continue tracks_processed += 1 # Set mit bestehenden Memory Cue Positionen erstellen (für schnelle Suche) existing_memory_positions = set() for memory_cue in memory_cues_in_track: start_time = memory_cue.get("Start") if start_time: existing_memory_positions.add(start_time) # Hot Cues nach Start-Zeit sortieren, um den ersten zu identifizieren hot_cues_sorted = sorted(hot_cues_in_track, key=lambda cue: float(cue.get("Start", "0"))) # Spezialbehandlung: Ersten Hot Cue mit Namen "1.1Bars" in "Start" umbenennen if hot_cues_sorted and hot_cues_sorted[0].get("Name") == "1.1Bars": hot_cues_sorted[0].set("Name", "Start") first_hot_cue_start = hot_cues_sorted[0].get("Start", "N/A") first_hot_cue_num = hot_cues_sorted[0].get("Num", "N/A") print(f"Hot Cue {first_hot_cue_num} (Start: {first_hot_cue_start}) von '1.1Bars' zu 'Start' umbenannt") for i, hot_cue in enumerate(hot_cues_sorted): start_time = hot_cue.get("Start") hot_cue_num = hot_cue.get("Num", "N/A") hot_cue_name = hot_cue.get("Name", "") # Prüfen, ob bereits ein Memory Cue an dieser Position existiert if start_time and start_time in existing_memory_positions: print(f"Hot Cue {hot_cue_num} (Start: {start_time}) übersprungen - Memory Cue existiert bereits") total_hot_cues_skipped += 1 continue # Neues POSITION_MARK Element als Memory Cue erstellen memory_cue = ET.Element("POSITION_MARK") # Alle Attribute kopieren for attr_name, attr_value in hot_cue.attrib.items(): memory_cue.set(attr_name, attr_value) # Num auf -1 setzen (Memory Cue) memory_cue.set("Num", "-1") print(f"Hot Cue {hot_cue_num} (Start: {start_time}) als Memory Cue kopiert") # Farb-Attribute entfernen (Memory Cues haben keine Farben) for color_attr in ["Red", "Green", "Blue"]: if color_attr in memory_cue.attrib: del memory_cue.attrib[color_attr] # Memory Cue zum Track hinzufügen track.append(memory_cue) total_hot_cues_copied += 1 # Position zur Liste der existierenden Memory Cues hinzufügen if start_time: existing_memory_positions.add(start_time) # XML speichern output_path = Path(output_file_path) if output_file_path else xml_path try: tree.write(output_path, encoding="utf-8", xml_declaration=True) print(f"\nErgebnis gespeichert in: {output_path}") print(f"Verarbeitete Tracks: {tracks_processed}") print(f"Übersprungene Tracks: {tracks_skipped}") print(f"Kopierte Hot Cues: {total_hot_cues_copied}") print(f"Übersprungene Hot Cues: {total_hot_cues_skipped}") if tracks_skipped > 0: print("(Übersprungene Tracks: bereits mehr als 1 Memory Cue vorhanden oder nur 1 Hot Cue)") if total_hot_cues_skipped > 0: print("(Übersprungene Hot Cues: Memory Cue an gleicher Position bereits vorhanden)") except Exception as e: raise Exception(f"Fehler beim Speichern der XML-Datei: {e}") def convert_hot_cues_to_memory_cues(xml_file_path, output_file_path=None, backup=True): """ Konvertiert alle Hot Cues zu Memory Cues (ändert Num zu -1). Args: xml_file_path (str): Pfad zur Rekordbox XML Datei output_file_path (str, optional): Ausgabedatei. Wenn None, wird die Original-Datei überschrieben backup (bool): Erstellt ein Backup der Original-Datei """ xml_path = Path(xml_file_path) if not xml_path.exists(): raise FileNotFoundError(f"XML-Datei nicht gefunden: {xml_file_path}") # Backup erstellen if backup: backup_path = xml_path.with_suffix(xml_path.suffix + '.backup') shutil.copy2(xml_path, backup_path) print(f"Backup erstellt: {backup_path}") # XML laden try: tree = ET.parse(xml_path) root = tree.getroot() except ET.ParseError as e: raise Exception(f"Fehler beim Parsen der XML-Datei: {e}") converted_hot_cues = 0 # Alle POSITION_MARK Elemente finden for position_mark in root.findall(".//POSITION_MARK"): num_attr = position_mark.get("Num") # Hot Cues haben Num >= 0 if num_attr and num_attr.isdigit() and int(num_attr) >= 0: # Zu Memory Cue konvertieren position_mark.set("Num", "-1") # Farb-Attribute entfernen for color_attr in ["Red", "Green", "Blue"]: if color_attr in position_mark.attrib: del position_mark.attrib[color_attr] converted_hot_cues += 1 start_time = position_mark.get("Start", "N/A") print(f"Hot Cue (Start: {start_time}) zu Memory Cue konvertiert") # XML speichern output_path = Path(output_file_path) if output_file_path else xml_path try: tree.write(output_path, encoding="utf-8", xml_declaration=True) print(f"\nErgebnis gespeichert in: {output_path}") print(f"Konvertierte Hot Cues: {converted_hot_cues}") except Exception as e: raise Exception(f"Fehler beim Speichern der XML-Datei: {e}") def cleanup_empty_cues(xml_file_path, output_file_path=None, backup=True): """ Löscht alle Hot Cues und Memory Cues, deren Name leer ist oder nur aus Leerzeichen besteht. Args: xml_file_path (str): Pfad zur Rekordbox XML Datei output_file_path (str, optional): Ausgabedatei. Wenn None, wird die Original-Datei überschrieben backup (bool): Erstellt ein Backup der Original-Datei """ xml_path = Path(xml_file_path) if not xml_path.exists(): raise FileNotFoundError(f"XML-Datei nicht gefunden: {xml_file_path}") if backup: backup_path = xml_path.with_suffix(xml_path.suffix + '.backup') shutil.copy2(xml_path, backup_path) print(f"Backup erstellt: {backup_path}") try: tree = ET.parse(xml_path) root = tree.getroot() except ET.ParseError as e: raise Exception(f"Fehler beim Parsen der XML-Datei: {e}") cues_deleted = 0 for track in root.findall(".//TRACK"): position_marks = list(track.findall(".//POSITION_MARK")) for position_mark in position_marks: name = position_mark.get("Name", "") if name.strip() == "": track.remove(position_mark) cues_deleted += 1 output_path = Path(output_file_path) if output_file_path else xml_path try: tree.write(output_path, encoding="utf-8", xml_declaration=True) print(f"\nErgebnis gespeichert in: {output_path}") print(f"Gelöschte Cues: {cues_deleted}") except Exception as e: raise Exception(f"Fehler beim Speichern der XML-Datei: {e}") def main(): parser = argparse.ArgumentParser( description="Rekordbox XML Hot Cue Tools", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Beispiele: # Hot Cues als Memory Cues kopieren python main.py copy rekordbox.xml # Hot Cues als Memory Cues kopieren und in neue Datei speichern python main.py copy rekordbox.xml -o rekordbox_modified.xml # Alle Hot Cues zu Memory Cues konvertieren python main.py convert rekordbox.xml # Ohne Backup arbeiten python main.py copy rekordbox.xml --no-backup """ ) subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle") # Copy command copy_parser = subparsers.add_parser("copy", help="Hot Cues als Memory Cues kopieren") copy_parser.add_argument("xml_file", help="Pfad zur Rekordbox XML Datei") copy_parser.add_argument("-o", "--output", help="Ausgabedatei (optional)") copy_parser.add_argument("--no-backup", action="store_true", help="Kein Backup erstellen") # Convert command convert_parser = subparsers.add_parser("convert", help="Hot Cues zu Memory Cues konvertieren") convert_parser.add_argument("xml_file", help="Pfad zur Rekordbox XML Datei") convert_parser.add_argument("-o", "--output", help="Ausgabedatei (optional)") convert_parser.add_argument("--no-backup", action="store_true", help="Kein Backup erstellen") # Cleanup command cleanup_parser = subparsers.add_parser("cleanup", help="Leere Hot Cues und Memory Cues löschen") cleanup_parser.add_argument("xml_file", help="Pfad zur Rekordbox XML Datei") cleanup_parser.add_argument("-o", "--output", help="Ausgabedatei (optional)") cleanup_parser.add_argument("--no-backup", action="store_true", help="Kein Backup erstellen") args = parser.parse_args() if not args.command: parser.print_help() return try: if args.command == "copy": copy_hot_cues_to_memory_cues( args.xml_file, args.output, backup=not args.no_backup ) elif args.command == "convert": convert_hot_cues_to_memory_cues( args.xml_file, args.output, backup=not args.no_backup ) elif args.command == "cleanup": cleanup_empty_cues( args.xml_file, args.output, backup=not args.no_backup ) except Exception as e: print(f"Fehler: {e}") return 1 if __name__ == "__main__": main()