Skip to content

Commit

Permalink
add playlist support to watcher + virtual fs (minus adding a track)
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline committed Oct 25, 2023
1 parent 85201dc commit 2c05a24
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 80 deletions.
18 changes: 9 additions & 9 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ def seeded_cache(config: Config) -> None:
, ('r2' , 'Native State', 'Native State');
INSERT INTO tracks
(id , source_path , source_mtime, virtual_filename, title , release_id, track_number, disc_number, duration_seconds, formatted_artists)
VALUES ('t1', '{musicpaths[0]}', '999' , '01.m4a' , 'Track 1', 'r1' , '01' , '01' , 120 , 'Techno Man;Bass Man')
, ('t2', '{musicpaths[1]}', '999' , '02.m4a' , 'Track 2', 'r1' , '02' , '01' , 240 , 'Techno Man;Bass Man')
, ('t3', '{musicpaths[2]}', '999' , '01.m4a' , 'Track 1', 'r2' , '01' , '01' , 120 , 'Violin Woman feat. Conductor Woman')
, ('t4', '{musicpaths[3]}', '999' , '01.m4a' , 'Track 1', 'r3' , '01' , '01' , 120 , '');
(id , source_path , source_mtime, virtual_filename, formatted_release_position, title , release_id, track_number, disc_number, duration_seconds, formatted_artists)
VALUES ('t1', '{musicpaths[0]}', '999' , '01.m4a' , '01' , 'Track 1', 'r1' , '01' , '01' , 120 , 'Techno Man;Bass Man')
, ('t2', '{musicpaths[1]}', '999' , '02.m4a' , '02' , 'Track 2', 'r1' , '02' , '01' , 240 , 'Techno Man;Bass Man')
, ('t3', '{musicpaths[2]}', '999' , '01.m4a' , '01' , 'Track 1', 'r2' , '01' , '01' , 120 , 'Violin Woman feat. Conductor Woman')
, ('t4', '{musicpaths[3]}', '999' , '01.m4a' , '02' , 'Track 1', 'r3' , '01' , '01' , 120 , '');
INSERT INTO releases_artists
(release_id, artist , artist_sanitized , role , alias)
Expand All @@ -150,8 +150,8 @@ def seeded_cache(config: Config) -> None:
INSERT INTO collages_releases
(collage_name, release_id, position)
VALUES ('Rose Gold' , 'r1' , 0)
, ('Rose Gold' , 'r2' , 1);
VALUES ('Rose Gold' , 'r1' , 1)
, ('Rose Gold' , 'r2' , 2);
INSERT INTO playlists
(name , source_mtime)
Expand All @@ -160,8 +160,8 @@ def seeded_cache(config: Config) -> None:
INSERT INTO playlists_tracks
(playlist_name, track_id, position)
VALUES ('Lala Lisa' , 't1' , 0)
, ('Lala Lisa' , 't3' , 1);
VALUES ('Lala Lisa' , 't1' , 1)
, ('Lala Lisa' , 't3' , 2);
""" # noqa: E501
)

Expand Down
6 changes: 5 additions & 1 deletion rose/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
STATE_HOME.mkdir(parents=True, exist_ok=True)
LOGFILE = STATE_HOME / "rose.log"

# Useful for debugging problems with the virtual FS, since pytest doesn't capture that debug logging
# output.
LOG_EVEN_THOUGH_WERE_IN_TEST = True

# Add a logging handler for stdout unless we are testing. Pytest
# captures logging output on its own.
if "pytest" not in sys.modules: # pragma: no cover
if "pytest" not in sys.modules or LOG_EVEN_THOUGH_WERE_IN_TEST: # pragma: no cover
stream_formatter = logging.Formatter(
"[%(asctime)s] %(levelname)s: %(message)s",
datefmt="%H:%M:%S",
Expand Down
77 changes: 54 additions & 23 deletions rose/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ class CachedTrack:
release_id: str
track_number: str
disc_number: str
formatted_release_position: str
duration_seconds: int

artists: list[CachedArtist]
Expand Down Expand Up @@ -453,6 +454,7 @@ def _update_cache_for_releases_executor(
, t.release_id
, t.track_number
, t.disc_number
, t.formatted_release_position
, t.duration_seconds
, t.formatted_artists
, COALESCE(a.names, '') AS artist_names
Expand Down Expand Up @@ -482,6 +484,7 @@ def _update_cache_for_releases_executor(
release_id=row["release_id"],
track_number=row["track_number"],
disc_number=row["disc_number"],
formatted_release_position=row["formatted_release_position"],
duration_seconds=row["duration_seconds"],
artists=track_artists,
formatted_artists=row["formatted_artists"],
Expand Down Expand Up @@ -773,8 +776,12 @@ def _update_cache_for_releases_executor(
virtual_filename="",
title=tags.title or "Unknown Title",
release_id=release.id,
track_number=tags.track_number or "1",
disc_number=tags.disc_number or "1",
# Remove `.` here because we use `.` to parse out discno/trackno in the virtual
# filesystem. It should almost never happen, but better to be safe.
track_number=(tags.track_number or "1").replace(".", ""),
disc_number=(tags.disc_number or "1").replace(".", ""),
# This is calculated with the virtual filename.
formatted_release_position="",
duration_seconds=tags.duration_sec,
artists=[],
formatted_artists=format_artist_string(tags.artists, release.genres),
Expand All @@ -788,8 +795,8 @@ def _update_cache_for_releases_executor(
track.artists.append(CachedArtist(name=alias, role=role, alias=True))
track_ids_to_insert.add(track.id)

# Now calculate whether this release is multidisc, and then assign virtual_filenames for
# each track that lacks one.
# Now calculate whether this release is multidisc, and then assign virtual_filenames and
# formatted_release_positions for each track that lacks one.
multidisc = len({t.disc_number for t in tracks}) > 1
if release.multidisc != multidisc:
logger.debug(f"Release multidisc change detected for {source_path}, updating")
Expand All @@ -798,11 +805,20 @@ def _update_cache_for_releases_executor(
# Use this set to avoid name collisions.
seen_track_names: set[str] = set()
for i, t in enumerate(tracks):
virtual_filename = ""
formatted_release_position = ""
if multidisc and t.disc_number:
virtual_filename += f"{t.disc_number:0>2}-"
formatted_release_position += f"{t.disc_number:0>2}-"
if t.track_number:
virtual_filename += f"{t.track_number:0>2}. "
formatted_release_position += f"{t.track_number:0>2}"
if formatted_release_position != t.formatted_release_position:
logger.debug(
f"Track formatted release position change detected for {t.source_path}, "
"updating"
)
tracks[i].formatted_release_position = formatted_release_position
track_ids_to_insert.add(t.id)

virtual_filename = ""
virtual_filename += f"{t.formatted_artists} - "
virtual_filename += t.title or "Unknown Title"
virtual_filename += t.source_path.suffix
Expand Down Expand Up @@ -873,6 +889,7 @@ def _update_cache_for_releases_executor(
track.release_id,
track.track_number,
track.disc_number,
track.formatted_release_position,
track.duration_seconds,
track.formatted_artists,
]
Expand Down Expand Up @@ -989,20 +1006,22 @@ def _update_cache_for_releases_executor(
, release_id
, track_number
, disc_number
, formatted_release_position
, duration_seconds
, formatted_artists
)
VALUES {",".join(["(?,?,?,?,?,?,?,?,?,?)"]*len(upd_track_args))}
VALUES {",".join(["(?,?,?,?,?,?,?,?,?,?,?)"]*len(upd_track_args))}
ON CONFLICT (id) DO UPDATE SET
source_path = excluded.source_path
, source_mtime = excluded.source_mtime
, virtual_filename = excluded.virtual_filename
, title = excluded.title
, release_id = excluded.release_id
, track_number = excluded.track_number
, disc_number = excluded.disc_number
, duration_seconds = excluded.duration_seconds
, formatted_artists = excluded.formatted_artists
source_path = excluded.source_path
, source_mtime = excluded.source_mtime
, virtual_filename = excluded.virtual_filename
, title = excluded.title
, release_id = excluded.release_id
, track_number = excluded.track_number
, disc_number = excluded.disc_number
, formatted_release_position = excluded.formatted_release_position
, duration_seconds = excluded.duration_seconds
, formatted_artists = excluded.formatted_artists
""",
_flatten(upd_track_args),
)
Expand Down Expand Up @@ -1095,7 +1114,12 @@ def update_cache_for_collages(
release_ids=[],
)

source_mtime = str(f.stat().st_mtime)
try:
source_mtime = str(f.stat().st_mtime)
except FileNotFoundError:
# Collage was deleted... continue without doing anything. It will be cleaned up by
# the eviction function.
continue
if source_mtime == cached_collage.source_mtime and not force:
logger.debug(f"Collage cache hit (mtime) for {source_path}, reusing cached data")
continue
Expand Down Expand Up @@ -1256,7 +1280,12 @@ def update_cache_for_playlists(
track_ids=[],
)

source_mtime = str(f.stat().st_mtime)
try:
source_mtime = str(f.stat().st_mtime)
except FileNotFoundError:
# Playlist was deleted... continue without doing anything. It will be cleaned up by
# the eviction function.
continue
if source_mtime == cached_playlist.source_mtime and not force:
logger.debug(f"playlist cache hit (mtime) for {source_path}, reusing cached data")
continue
Expand Down Expand Up @@ -1556,6 +1585,7 @@ def get_release(
, t.release_id
, t.track_number
, t.disc_number
, t.formatted_release_position
, t.duration_seconds
, t.formatted_artists
, COALESCE(a.names, '') AS artist_names
Expand Down Expand Up @@ -1586,6 +1616,7 @@ def get_release(
release_id=row["release_id"],
track_number=row["track_number"],
disc_number=row["disc_number"],
formatted_release_position=row["formatted_release_position"],
duration_seconds=row["duration_seconds"],
formatted_artists=row["formatted_artists"],
artists=tartists,
Expand Down Expand Up @@ -1670,12 +1701,12 @@ def list_playlists(c: Config) -> Iterator[str]:
yield row["name"]


def list_playlist_tracks(c: Config, playlist_name: str) -> Iterator[tuple[int, str, Path]]:
"""Returns tuples of (position, track_virtual_filename, track_source_path)."""
def list_playlist_tracks(c: Config, playlist_name: str) -> Iterator[tuple[int, str, str, Path]]:
"""Returns tuples of (position, track_id, track_virtual_filename, track_source_path)."""
with connect(c) as conn:
cursor = conn.execute(
"""
SELECT pt.position, t.virtual_filename, t.source_path
SELECT pt.position, t.id, t.virtual_filename, t.source_path
FROM playlists_tracks pt
JOIN tracks t ON t.id = pt.track_id
WHERE pt.playlist_name = ?
Expand All @@ -1684,7 +1715,7 @@ def list_playlist_tracks(c: Config, playlist_name: str) -> Iterator[tuple[int, s
(playlist_name,),
)
for row in cursor:
yield (row["position"], row["virtual_filename"], Path(row["source_path"]))
yield (row["position"], row["id"], row["virtual_filename"], Path(row["source_path"]))


def list_collages(c: Config) -> Iterator[str]:
Expand Down
4 changes: 4 additions & 0 deletions rose/cache.sql
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ CREATE TABLE tracks (
release_id TEXT NOT NULL REFERENCES releases(id) ON DELETE CASCADE,
track_number TEXT NOT NULL,
disc_number TEXT NOT NULL,
-- Formatted disc_number/track_number combination that prefixes the virtual_filename in the
-- release view. This can be derived on-the-fly, but doesn't hurt to compute it once and pull it
-- from the cache after.
formatted_release_position TEXT NOT NULL,
duration_seconds INTEGER NOT NULL,
-- This is its own state because ordering matters--we preserve the ordering in the tags.
-- However, the one-to-many table does not have ordering.
Expand Down
10 changes: 6 additions & 4 deletions rose/cache_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ def test_get_release(config: Config) -> None:
release_id="r1",
track_number="01",
disc_number="01",
formatted_release_position="01",
duration_seconds=120,
artists=[
CachedArtist(name="Bass Man", role="main", alias=False),
Expand All @@ -786,6 +787,7 @@ def test_get_release(config: Config) -> None:
release_id="r1",
track_number="02",
disc_number="01",
formatted_release_position="02",
duration_seconds=240,
artists=[
CachedArtist(name="Bass Man", role="main", alias=False),
Expand Down Expand Up @@ -854,8 +856,8 @@ def test_list_collages(config: Config) -> None:
def test_list_collage_releases(config: Config) -> None:
releases = list(list_collage_releases(config, "Rose Gold"))
assert set(releases) == {
(0, "r1", config.music_source_dir / "r1"),
(1, "r2", config.music_source_dir / "r2"),
(1, "r1", config.music_source_dir / "r1"),
(2, "r2", config.music_source_dir / "r2"),
}
releases = list(list_collage_releases(config, "Ruby Red"))
assert releases == []
Expand All @@ -871,8 +873,8 @@ def test_list_playlists(config: Config) -> None:
def test_list_playlist_tracks(config: Config) -> None:
tracks = list(list_playlist_tracks(config, "Lala Lisa"))
assert set(tracks) == {
(0, "01.m4a", config.music_source_dir / "r1" / "01.m4a"),
(1, "01.m4a", config.music_source_dir / "r2" / "01.m4a"),
(1, "t1", "01.m4a", config.music_source_dir / "r1" / "01.m4a"),
(2, "t3", "01.m4a", config.music_source_dir / "r2" / "01.m4a"),
}
tracks = list(list_playlist_tracks(config, "Turtle Rabbit"))
assert tracks == []
Expand Down
2 changes: 1 addition & 1 deletion rose/collages_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def test_rename_collage(config: Config, source_dir: Path) -> None:
def test_dump_collages(config: Config) -> None:
out = dump_collages(config)
# fmt: off
assert out == '{"Rose Gold": [{"position": 0, "release": "r1"}, {"position": 1, "release": "r2"}], "Ruby Red": []}' # noqa: E501
assert out == '{"Rose Gold": [{"position": 1, "release": "r1"}, {"position": 2, "release": "r2"}], "Ruby Red": []}' # noqa: E501
# fmt: on


Expand Down
10 changes: 8 additions & 2 deletions rose/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,14 @@ def dump_playlists(c: Config) -> str:
playlist_names = list(list_playlists(c))
for name in playlist_names:
out[name] = []
for pos, virtual_filename, _ in list_playlist_tracks(c, name):
out[name].append({"position": pos, "track": virtual_filename})
for pos, track_id, virtual_filename, _ in list_playlist_tracks(c, name):
out[name].append(
{
"position": pos,
"track_id": track_id,
"track_filename": virtual_filename,
}
)
return json.dumps(out)


Expand Down
2 changes: 1 addition & 1 deletion rose/playlists_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def test_rename_playlist(config: Config, source_dir: Path) -> None:
def test_dump_playlists(config: Config) -> None:
out = dump_playlists(config)
# fmt: off
assert out == '{"Lala Lisa": [{"position": 0, "track": "01.m4a"}, {"position": 1, "track": "01.m4a"}], "Turtle Rabbit": []}' # noqa: E501
assert out == '{"Lala Lisa": [{"position": 1, "track_id": "t1", "track_filename": "01.m4a"}, {"position": 2, "track_id": "t3", "track_filename": "01.m4a"}], "Turtle Rabbit": []}' # noqa: E501
# fmt: on


Expand Down
6 changes: 4 additions & 2 deletions rose/releases_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,12 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non
id=track_ids[0],
source_path=release_path / "01.m4a",
source_mtime=tracks[0].source_mtime,
virtual_filename="01. BLACKPINK - I Do Like That.m4a",
virtual_filename="BLACKPINK - I Do Like That.m4a",
title="I Do Like That",
release_id=release_id,
track_number="1",
disc_number="1",
formatted_release_position="01",
duration_seconds=2,
artists=[
CachedArtist(name="BLACKPINK", role="main", alias=False),
Expand All @@ -151,11 +152,12 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non
id=track_ids[1],
source_path=release_path / "02.m4a",
source_mtime=tracks[1].source_mtime,
virtual_filename="02. JISOO - All Eyes On Me.m4a",
virtual_filename="JISOO - All Eyes On Me.m4a",
title="All Eyes On Me",
release_id=release_id,
track_number="2",
disc_number="1",
formatted_release_position="02",
duration_seconds=2,
artists=[
CachedArtist(name="JISOO", role="main", alias=False),
Expand Down
Loading

0 comments on commit 2c05a24

Please sign in to comment.