Skip to content

Commit

Permalink
pass through cover art
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline committed Oct 11, 2023
1 parent 0152a30 commit 81ba357
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 43 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,13 @@ Every directory should follow the format: `$music_source_dir/$album_name/$track.

So for example: `$music_source_dir/BLACKPINK - 2016. SQUARE ONE/*.mp3`.

## Supported Filetypes
## Filetypes

Rosé supports MP3, M4A, OGG, OPUS, and FLAC audio files and JPG and PNG image
files.
Rosé supports MP3, M4A, OGG, OPUS, and FLAC audio files.

Rosé also supports JPEG and PNG cover art. The supported cover art file stems
are `cover`, `folder`, and `art`. The supported cover art file extensions are
`.jpg`, `.jpeg`, and `.png`.

## Tagging

Expand Down
24 changes: 13 additions & 11 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,21 @@ def seeded_cache(config: Config) -> Iterator[None]:
config.music_source_dir / "r1",
config.music_source_dir / "r2",
]
filepaths = [
musicpaths = [
config.music_source_dir / "r1" / "01.m4a",
config.music_source_dir / "r1" / "02.m4a",
config.music_source_dir / "r2" / "01.m4a",
]
imagepaths = [
config.music_source_dir / "r2" / "cover.jpg",
]

with sqlite3.connect(config.cache_database_path) as conn:
conn.executescript(
f"""\
INSERT INTO releases (id, source_path, virtual_dirname, title, release_type, release_year, new)
VALUES ('r1', '{dirpaths[0]}', 'r1', 'Release 1', 'album', 2023, true)
, ('r2', '{dirpaths[1]}', 'r2', 'Release 2', 'album', 2021, false);
INSERT INTO releases (id, source_path, cover_image_path, virtual_dirname, title, release_type, release_year, new)
VALUES ('r1', '{dirpaths[0]}', null, 'r1', 'Release 1', 'album', 2023, true)
, ('r2', '{dirpaths[1]}', '{imagepaths[0]}', 'r2', 'Release 2', 'album', 2021, false);
INSERT INTO releases_genres (release_id, genre, genre_sanitized)
VALUES ('r1', 'Techno', 'Techno')
Expand All @@ -75,11 +78,10 @@ def seeded_cache(config: Config) -> Iterator[None]:
VALUES ('r1', 'Silk Music', 'Silk Music')
, ('r2', 'Native State', 'Native State');
INSERT INTO tracks
(id, source_path, virtual_filename, title, release_id, track_number, disc_number, duration_seconds)
VALUES ('t1', '{filepaths[0]}', '01.m4a', 'Track 1', 'r1', '01', '01', 120)
, ('t2', '{filepaths[1]}', '02.m4a', 'Track 2', 'r1', '02', '01', 240)
, ('t3', '{filepaths[2]}', '01.m4a', 'Track 1', 'r2', '01', '01', 120);
INSERT INTO tracks (id, source_path, virtual_filename, title, release_id, track_number, disc_number, duration_seconds)
VALUES ('t1', '{musicpaths[0]}', '01.m4a', 'Track 1', 'r1', '01', '01', 120)
, ('t2', '{musicpaths[1]}', '02.m4a', 'Track 2', 'r1', '02', '01', 240)
, ('t3', '{musicpaths[2]}', '01.m4a', 'Track 1', 'r2', '01', '01', 120);
INSERT INTO releases_artists (release_id, artist, artist_sanitized, role)
VALUES ('r1', 'Techno Man', 'Techno Man', 'main')
Expand All @@ -94,12 +96,12 @@ def seeded_cache(config: Config) -> Iterator[None]:
, ('t2', 'Bass Man', 'Bass Man', 'main')
, ('t3', 'Violin Woman', 'Violin Woman', 'main')
, ('t3', 'Conductor Woman', 'Conductor Woman', 'guest');
"""
""" # noqa: E501
)

for d in dirpaths:
d.mkdir()
for f in filepaths:
for f in musicpaths + imagepaths:
f.touch()

yield
Expand Down
1 change: 1 addition & 0 deletions rose/cache/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class CachedArtist:
class CachedRelease:
id: str
source_path: Path
cover_image_path: Path | None
virtual_dirname: str
title: str
release_type: str
Expand Down
57 changes: 46 additions & 11 deletions rose/cache/read.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator

Expand Down Expand Up @@ -37,6 +38,7 @@ def list_releases(
SELECT
r.id
, r.source_path
, r.cover_image_path
, r.virtual_dirname
, r.title
, r.release_type
Expand Down Expand Up @@ -86,6 +88,7 @@ def list_releases(
yield CachedRelease(
id=row["id"],
source_path=Path(row["source_path"]),
cover_image_path=Path(row["cover_image_path"]) if row["cover_image_path"] else None,
virtual_dirname=row["virtual_dirname"],
title=row["title"],
release_type=row["release_type"],
Expand All @@ -97,7 +100,15 @@ def list_releases(
)


def list_tracks(c: Config, release_virtual_dirname: str) -> Iterator[CachedTrack]:
@dataclass
class ReleaseFiles:
tracks: list[CachedTrack]
cover: Path | None


def get_release_files(c: Config, release_virtual_dirname: str) -> ReleaseFiles:
rf = ReleaseFiles(tracks=[], cover=None)

with connect(c) as conn:
cursor = conn.execute(
r"""
Expand Down Expand Up @@ -131,18 +142,29 @@ def list_tracks(c: Config, release_virtual_dirname: str) -> Iterator[CachedTrack
artists: list[CachedArtist] = []
for n, r in zip(row["artist_names"].split(r" \\ "), row["artist_roles"].split(r" \\ ")):
artists.append(CachedArtist(name=n, role=r))
yield CachedTrack(
id=row["id"],
source_path=Path(row["source_path"]),
virtual_filename=row["virtual_filename"],
title=row["title"],
release_id=row["release_id"],
track_number=row["track_number"],
disc_number=row["disc_number"],
duration_seconds=row["duration_seconds"],
artists=artists,
rf.tracks.append(
CachedTrack(
id=row["id"],
source_path=Path(row["source_path"]),
virtual_filename=row["virtual_filename"],
title=row["title"],
release_id=row["release_id"],
track_number=row["track_number"],
disc_number=row["disc_number"],
duration_seconds=row["duration_seconds"],
artists=artists,
)
)

cursor = conn.execute(
"SELECT cover_image_path FROM releases WHERE virtual_dirname = ?",
(release_virtual_dirname,),
)
if (row := cursor.fetchone()) and row["cover_image_path"]:
rf.cover = Path(row["cover_image_path"])

return rf


def list_artists(c: Config) -> Iterator[str]:
with connect(c) as conn:
Expand Down Expand Up @@ -197,6 +219,19 @@ def track_exists(
return None


def cover_exists(c: Config, release_virtual_dirname: str, cover_name: str) -> Path | None:
with connect(c) as conn:
cursor = conn.execute(
"SELECT cover_image_path FROM releases r WHERE r.virtual_dirname = ?",
(release_virtual_dirname,),
)
if (row := cursor.fetchone()) and row["cover_image_path"]:
p = Path(row["cover_image_path"])
if p.name == cover_name:
return p
return None


def artist_exists(c: Config, artist_sanitized: str) -> bool:
with connect(c) as conn:
cursor = conn.execute(
Expand Down
25 changes: 21 additions & 4 deletions rose/cache/read_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from rose.cache.dataclasses import CachedArtist, CachedRelease, CachedTrack
from rose.cache.read import (
artist_exists,
cover_exists,
genre_exists,
get_release_files,
label_exists,
list_artists,
list_genres,
list_labels,
list_releases,
list_tracks,
release_exists,
track_exists,
)
Expand All @@ -25,6 +26,7 @@ def test_list_releases(config: Config) -> None:
CachedRelease(
id="r1",
source_path=Path(config.music_source_dir / "r1"),
cover_image_path=None,
virtual_dirname="r1",
title="Release 1",
release_type="album",
Expand All @@ -40,6 +42,7 @@ def test_list_releases(config: Config) -> None:
CachedRelease(
id="r2",
source_path=Path(config.music_source_dir / "r2"),
cover_image_path=Path(config.music_source_dir / "r2" / "cover.jpg"),
virtual_dirname="r2",
title="Release 2",
release_type="album",
Expand All @@ -58,6 +61,7 @@ def test_list_releases(config: Config) -> None:
CachedRelease(
id="r1",
source_path=Path(config.music_source_dir / "r1"),
cover_image_path=None,
virtual_dirname="r1",
title="Release 1",
release_type="album",
Expand All @@ -76,6 +80,7 @@ def test_list_releases(config: Config) -> None:
CachedRelease(
id="r1",
source_path=Path(config.music_source_dir / "r1"),
cover_image_path=None,
virtual_dirname="r1",
title="Release 1",
release_type="album",
Expand All @@ -94,6 +99,7 @@ def test_list_releases(config: Config) -> None:
CachedRelease(
id="r1",
source_path=Path(config.music_source_dir / "r1"),
cover_image_path=None,
virtual_dirname="r1",
title="Release 1",
release_type="album",
Expand All @@ -110,9 +116,9 @@ def test_list_releases(config: Config) -> None:


@pytest.mark.usefixtures("seeded_cache")
def test_list_tracks(config: Config) -> None:
tracks = list(list_tracks(config, "r1"))
assert tracks == [
def test_get_release_files(config: Config) -> None:
rf = get_release_files(config, "r1")
assert rf.tracks == [
CachedTrack(
id="t1",
source_path=Path(config.music_source_dir / "r1" / "01.m4a"),
Expand Down Expand Up @@ -142,6 +148,10 @@ def test_list_tracks(config: Config) -> None:
],
),
]
assert rf.cover is None

rf = get_release_files(config, "r2")
assert rf.cover == config.music_source_dir / "r2" / "cover.jpg"


@pytest.mark.usefixtures("seeded_cache")
Expand Down Expand Up @@ -175,6 +185,13 @@ def test_track_exists(config: Config) -> None:
assert not track_exists(config, "r1", "lalala")


@pytest.mark.usefixtures("seeded_cache")
def test_cover_exists(config: Config) -> None:
assert cover_exists(config, "r2", "cover.jpg")
assert not cover_exists(config, "r2", "cover.png")
assert not cover_exists(config, "r1", "cover.jpg")


@pytest.mark.usefixtures("seeded_cache")
def test_artist_exists(config: Config) -> None:
assert artist_exists(config, "Bass Man")
Expand Down
1 change: 1 addition & 0 deletions rose/cache/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ INSERT INTO release_type_enum (value) VALUES
CREATE TABLE releases (
id TEXT PRIMARY KEY,
source_path TEXT NOT NULL UNIQUE,
cover_image_path TEXT,
virtual_dirname TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
release_type TEXT NOT NULL REFERENCES release_type_enum(value),
Expand Down
23 changes: 21 additions & 2 deletions rose/cache/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

logger = logging.getLogger(__name__)

VALID_COVER_FILENAMES = [
stem + ext for stem in ["cover", "folder", "art"] for ext in [".jpg", ".jpeg", ".png"]
]

SUPPORTED_EXTENSIONS = [
".mp3",
".m4a",
Expand Down Expand Up @@ -128,9 +132,19 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path:
break
virtual_dirname = f"{original_virtual_dirname} [{collision_no}]"

# Search for cover art.
cover_image_path = None
for cn in VALID_COVER_FILENAMES:
p = release_dir / cn
if p.is_file():
cover_image_path = p.resolve()
break

# Construct the cached release.
release = CachedRelease(
id=release_id,
source_path=release_dir.resolve(),
cover_image_path=cover_image_path,
virtual_dirname=virtual_dirname,
title=tags.album or "Unknown Release",
release_type=(
Expand All @@ -149,13 +163,16 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path:
for name in names:
release.artists.append(CachedArtist(name=name, role=role))

# Upsert the release.
conn.execute(
"""
INSERT INTO releases
(id, source_path, virtual_dirname, title, release_type, release_year, new)
VALUES (?, ?, ?, ?, ?, ?, ?)
(id, source_path, cover_image_path, virtual_dirname, title, release_type,
release_year, new)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
source_path = ?,
cover_image_path = ?,
virtual_dirname = ?,
title = ?,
release_type = ?,
Expand All @@ -165,12 +182,14 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path:
(
release.id,
str(release.source_path),
str(release.cover_image_path),
release.virtual_dirname,
release.title,
release.release_type,
release.release_year,
release.new,
str(release.source_path),
str(release.cover_image_path),
release.virtual_dirname,
release.title,
release.release_type,
Expand Down
Loading

0 comments on commit 81ba357

Please sign in to comment.