Created
June 24, 2026 19:26
-
-
Save monostere0/71d9206bbd82feb4252f7b8ad75aa3f8 to your computer and use it in GitHub Desktop.
Removes files that are in your Rekordbox playlist but have been deleted from your drive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| clean_missing_playlist.py | |
| Remove all entries from a Rekordbox playlist whose linked audio file | |
| no longer exists on disk. | |
| By default it scans the playlist, reports every entry with a missing | |
| file, then asks for confirmation before removing them. | |
| Requires: pyrekordbox (pip install pyrekordbox) | |
| Examples: | |
| # Scan, report missing files, then ask before deleting | |
| python clean_missing_playlist.py --playlist "My Playlist" | |
| # Report only, never delete (no prompt either) | |
| python clean_missing_playlist.py --playlist "My Playlist" --dry-run | |
| # Skip the confirmation prompt (e.g. for scripting/cron) | |
| python clean_missing_playlist.py --playlist "My Playlist" --yes | |
| # Target a playlist by its ID instead of name | |
| python clean_missing_playlist.py --playlist-id 123456789 | |
| # Supply the database decryption key manually (only needed if | |
| # pyrekordbox can't find/derive it automatically) | |
| python clean_missing_playlist.py --playlist "My Playlist" --key <KEY> | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| def open_db(key=None): | |
| try: | |
| from pyrekordbox import Rekordbox6Database | |
| except ImportError as e: | |
| sys.exit(f"ERROR: pyrekordbox not installed: {e}\nRun: pip install pyrekordbox") | |
| try: | |
| return Rekordbox6Database(key=key) if key else Rekordbox6Database() | |
| except Exception as e: | |
| sys.exit( | |
| f"ERROR opening rekordbox database: {e}\n" | |
| "Make sure Rekordbox is closed (it locks the database) and has " | |
| "been opened at least once on this machine." | |
| ) | |
| def get_target_playlist(db, name, playlist_id): | |
| if playlist_id is not None: | |
| try: | |
| return db.get_playlist(ID=playlist_id).one() | |
| except Exception: | |
| sys.exit(f"Error: no playlist found with ID {playlist_id!r}.") | |
| matches = db.get_playlist(Name=name).all() | |
| if not matches: | |
| available = sorted(pl.Name for pl in db.get_playlist().all()) | |
| print(f"Playlist {name!r} not found. Available playlists:") | |
| for pl_name in available: | |
| print(f" - {pl_name}") | |
| sys.exit(1) | |
| if len(matches) > 1: | |
| ids = ", ".join(str(p.ID) for p in matches) | |
| sys.exit( | |
| f"Error: multiple playlists named {name!r} found (IDs: {ids}). " | |
| "Use --playlist-id to disambiguate." | |
| ) | |
| return matches[0] | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Remove playlist entries whose audio file is missing on disk." | |
| ) | |
| target = parser.add_mutually_exclusive_group(required=True) | |
| target.add_argument("--playlist", help="Name of the playlist to clean.") | |
| target.add_argument( | |
| "--playlist-id", type=int, help="ID of the playlist to clean." | |
| ) | |
| parser.add_argument( | |
| "--dry-run", | |
| action="store_true", | |
| help="Show what would be removed without modifying the database.", | |
| ) | |
| parser.add_argument( | |
| "--yes", "-y", | |
| action="store_true", | |
| help="Skip the confirmation prompt and remove missing entries immediately.", | |
| ) | |
| parser.add_argument( | |
| "--key", | |
| default=None, | |
| help="Rekordbox database decryption key (rarely needed manually).", | |
| ) | |
| args = parser.parse_args() | |
| db = open_db(args.key) | |
| playlist = get_target_playlist(db, args.playlist, args.playlist_id) | |
| print(f"Scanning playlist {playlist.Name!r} (ID {playlist.ID})...") | |
| songs = list(playlist.Songs) | |
| missing = [] | |
| for song in songs: | |
| content = song.Content | |
| path = content.FolderPath if content is not None else None | |
| if not path or not os.path.isfile(path): | |
| missing.append((song, content, path)) | |
| if not missing: | |
| print(f"No missing files found among {len(songs)} entries. Nothing to do.") | |
| return | |
| print(f"Found {len(missing)} of {len(songs)} entries with missing files:") | |
| for _, content, path in missing: | |
| title = content.Title if content is not None else "<unknown track>" | |
| print(f" - {title} ({path or '<no file path>'})") | |
| if args.dry_run: | |
| print("\nDry run: no changes made.") | |
| return | |
| if not args.yes: | |
| answer = input(f"\nRemove these {len(missing)} entries from {playlist.Name!r}? [y/N] ").strip().lower() | |
| if answer not in ("y", "yes"): | |
| print("Aborted. No changes made.") | |
| return | |
| for song, _, _ in missing: | |
| db.remove_from_playlist(playlist, song) | |
| db.commit() | |
| print(f"\nRemoved {len(missing)} entries from {playlist.Name!r} and saved changes.") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment