diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 627a0cb..0000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -branch = True -# fusepy does not support coverage collection. -omit = - rose/__main__.py - rose/virtualfs.py - setup.py diff --git a/.gitignore b/.gitignore index 71c5d74..6fc2afe 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ __pycache__ .ruff_cache .mypy_cache -.coverage +.coverage* result db.sqlite3 htmlcov diff --git a/README.md b/README.md index 2f07c28..4fadc80 100644 --- a/README.md +++ b/README.md @@ -183,8 +183,8 @@ The `music_source_dir` must be a flat directory of releases, meaning all release must be top-level directories inside `music_source_dir`. Each release should also be a single directory in `music_source_dir`. -Every directory should follow the format: `$music_source_dir/$release_name/$track.mp3`. -Additional nested directories are not currently supported. +Every directory should follow the format: `$music_source_dir/$release_name/**/$track.mp3`. +A release can have arbitrarily many nested subdirectories. So for example: `$music_source_dir/BLACKPINK - 2016. SQUARE ONE/*.mp3`. diff --git a/rose/cache.py b/rose/cache.py index 64ce6bd..25804d6 100644 --- a/rose/cache.py +++ b/rose/cache.py @@ -3,6 +3,7 @@ import math import multiprocessing import os +import os.path import re import sqlite3 import time @@ -330,19 +331,20 @@ def _update_cache_for_releases_executor( ) -> None: """The implementation logic, split out for multiprocessing.""" # First, call readdir on every release directory. We store the results in a map of - # Path Basename -> (Release ID if exists, File DirEntries). - dir_tree: list[tuple[Path, str | None, list[os.DirEntry[str]]]] = [] + # Path Basename -> (Release ID if exists, filenames). + dir_tree: list[tuple[Path, str | None, list[str]]] = [] release_uuids: list[str] = [] for rd in release_dirs: release_id = None - files: list[os.DirEntry[str]] = [] + files: list[str] = [] if not rd.is_dir(): logger.debug(f"Skipping scanning {rd} because it is not a directory") continue - for f in os.scandir(str(rd)): - if m := STORED_DATA_FILE_REGEX.match(f.name): - release_id = m[1] - files.append(f) + for root, _, fx in os.walk(str(rd)): + for f in fx: + if m := STORED_DATA_FILE_REGEX.match(f): + release_id = m[1] + files.append(os.path.join(root, f)) dir_tree.append((rd.resolve(), release_id, files)) if release_id is not None: release_uuids.append(release_id) @@ -512,7 +514,7 @@ def _update_cache_for_releases_executor( # any tracks, skip it. And if it does not have any tracks, but is in the cache, remove # it from the cache. for f in files: - if any(f.name.lower().endswith(ext) for ext in SUPPORTED_EXTENSIONS): + if any(f.lower().endswith(ext) for ext in SUPPORTED_EXTENSIONS): break else: logger.debug(f"Did not find any audio files in release {source_path}, skipping") @@ -610,7 +612,9 @@ def _update_cache_for_releases_executor( # Handle cover art change. try: - cover = next(Path(f.path).resolve() for f in files if f.name in VALID_COVER_FILENAMES) + cover = next( + Path(f).resolve() for f in files if os.path.basename(f) in VALID_COVER_FILENAMES + ) except StopIteration: # No cover art in directory. cover = None if cover != release.cover_image_path: @@ -642,21 +646,22 @@ def _update_cache_for_releases_executor( # tags. pulled_release_tags = False for f in files: - if not any(f.name.lower().endswith(ext) for ext in SUPPORTED_EXTENSIONS): + if not any(os.path.basename(f).lower().endswith(ext) for ext in SUPPORTED_EXTENSIONS): continue - track_path = Path(f.path).resolve() - cached_track = cached_tracks.get(str(track_path), None) - track_mtime = str(os.stat(track_path).st_mtime) + cached_track = cached_tracks.get(f, None) + track_mtime = str(os.stat(f).st_mtime) # Skip re-read if we can reuse a cached entry. if cached_track and track_mtime == cached_track.source_mtime and not force: - logger.debug(f"Track cache hit (mtime) for {f.name}, reusing cached data") + logger.debug( + f"Track cache hit (mtime) for {os.path.basename(f)}, reusing cached data" + ) tracks.append(cached_track) - unknown_cached_tracks.remove(str(track_path)) + unknown_cached_tracks.remove(f) continue # Otherwise, read tags from disk and construct a new cached_track. - logger.debug(f"Track cache miss for {f.name}, reading tags from disk") - tags = AudioFile.from_file(track_path) + logger.debug(f"Track cache miss for {os.path.basename(f)}, reading tags from disk") + tags = AudioFile.from_file(Path(f)) # Now that we're here, pull the release tags. We also need them to compute the # formatted artist string. @@ -771,7 +776,7 @@ def _update_cache_for_releases_executor( # And now create the cached track. track = CachedTrack( id=track_id, - source_path=track_path, + source_path=Path(f), source_mtime=track_mtime, virtual_filename="", title=tags.title or "Unknown Title",