diff --git a/README.md b/README.md index 5ac9cf3..c3f5ff5 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ export LIDARR_API_KEY="your-lidarr-api-key" | `--search-all` | Search all albums when adding an artist to Lidarr. | | `--repair` | Re-tag existing downloads under `--root` from source metadata (see below). | | `--retag-from-path` | Offline: re-tag artist/title from folder + filename (see below). | +| `-x`, `--exclude NAME` | Folder under `--root` to skip during `--repair`/`--retag-from-path` (repeatable). | | `--debug` | Verbose output. | ### Examples @@ -154,6 +155,9 @@ artist/title and leaves album/year alone. ```bash ./musicfetch --retag-from-path -d # preview ./musicfetch --retag-from-path -o /media/music + +# Skip folders (e.g. hand-curated playlists you don't want re-tagged) +./musicfetch --repair -x Unsorted -x playlists ``` ### 📁 Output Structure diff --git a/musicfetch b/musicfetch index 146f877..0df5be6 100755 --- a/musicfetch +++ b/musicfetch @@ -939,13 +939,13 @@ def repair_file(path: str, source: str, dry_run: bool) -> list[str]: return changed -def repair_library(root: str, dry_run: bool) -> tuple[int, int]: +def repair_library(root: str, dry_run: bool, exclude=()) -> tuple[int, int]: """Walk /// and re-tag audio files. Returns (scanned, changed).""" if not os.path.isdir(root): err(f"Root folder not found: {root}") return 0, 0 scanned = changed = 0 - for path, source, _artist in _iter_source_files(root): + for path, source, _artist in _iter_source_files(root, exclude): scanned += 1 try: if repair_file(path, source, dry_run): @@ -957,14 +957,20 @@ def repair_library(root: str, dry_run: bool) -> tuple[int, int]: return scanned, changed -def _iter_source_files(root: str): +def _iter_source_files(root: str, exclude=()): """Yield (path, source, artist) for audio files under /// - where source is a yt-dlp source folder (Lidarr album folders are skipped).""" + where source is a yt-dlp source folder (Lidarr album folders are skipped). + Skips any artist or source folder whose name is in `exclude` (case-insensitive).""" + skip = {e.lower() for e in exclude} for artist in sorted(os.listdir(root)): + if artist.lower() in skip: + continue adir = os.path.join(root, artist) if not os.path.isdir(adir): continue for source in sorted(os.listdir(adir)): + if source.lower() in skip: + continue sdir = os.path.join(adir, source) if not os.path.isdir(sdir) or not _is_source_dir(source): continue @@ -1029,13 +1035,13 @@ def retag_file_from_path(path: str, folder_artist: str, dry_run: bool) -> list[s return changed -def retag_library_from_path(root: str, dry_run: bool) -> tuple[int, int]: +def retag_library_from_path(root: str, dry_run: bool, exclude=()) -> tuple[int, int]: """Re-tag artist/title offline from folder+filename for every source file.""" if not os.path.isdir(root): err(f"Root folder not found: {root}") return 0, 0 scanned = changed = 0 - for path, _source, artist in _iter_source_files(root): + for path, _source, artist in _iter_source_files(root, exclude): scanned += 1 try: if retag_file_from_path(path, artist, dry_run): @@ -1087,6 +1093,9 @@ def parse_args(): p.add_argument("--retag-from-path", action="store_true", help="Offline: re-tag artist/title from folder + filename " "(fixes tags damaged by a prior --repair).") + p.add_argument("-x", "--exclude", action="append", default=[], metavar="NAME", + help="Folder name under --root to skip during --repair/--retag-from-path " + "(repeatable, e.g. -x Unsorted -x playlists).") p.add_argument("--debug", action="store_true", help="Verbose output.") return p.parse_args() @@ -1098,11 +1107,11 @@ def main(): query = " ".join(args.query).strip() if args.retag_from_path: - retag_library_from_path(args.root, args.dry_run) + retag_library_from_path(args.root, args.dry_run, args.exclude) return if args.repair: - repair_library(args.root, args.dry_run) + repair_library(args.root, args.dry_run, args.exclude) return if not query: diff --git a/tests/test_repair.py b/tests/test_repair.py index 6a637c2..4f82fd4 100644 --- a/tests/test_repair.py +++ b/tests/test_repair.py @@ -159,6 +159,23 @@ def test_repair_library_missing_root(): assert mf.repair_library("/no/such/dir", dry_run=False) == (0, 0) +def test_repair_library_exclude_skips_folders(tmp_path, monkeypatch): + root = tmp_path + (root / "Daft Punk" / "youtube").mkdir(parents=True) + (root / "Daft Punk" / "youtube" / f"A [{YT_ID}].opus").write_text("x") + (root / "Unsorted" / "youtube").mkdir(parents=True) # excluded artist folder + (root / "Unsorted" / "youtube" / f"B [{YT_ID}].opus").write_text("x") + (root / "Ephixa" / "playlists").mkdir(parents=True) # excluded source folder + (root / "Ephixa" / "playlists" / f"C [{YT_ID}].opus").write_text("x") + + visited = [] + monkeypatch.setattr(mf, "repair_file", + lambda path, source, dry_run: visited.append(path) or ["x"]) + scanned, _ = mf.repair_library(str(root), dry_run=False, exclude=["unsorted", "playlists"]) + assert scanned == 1 + assert visited and "Daft Punk" in visited[0] + + # ---- offline retag-from-path ---- def test_title_from_filename(): assert mf._title_from_filename(f"Song [{YT_ID}].opus") == "Song"