Skip to content

Commit

Permalink
add functionality to set cover art through cli+vfs
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline committed Oct 30, 2023
1 parent 80b0cb2 commit d5f3c25
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 77 deletions.
70 changes: 56 additions & 14 deletions docs/PLAYLISTS_COLLAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,14 @@ $ cd $fuse_mount_dir

$ mkdir "7. Collages/Morning"

$ tree "7. Collages"
$ tree "7. Collages/"
1. Collages/
├── Morning/...
└── Road Trip/...

$ mkdir "8. Playlists/Evening"

$ tree "8. Playlists"
$ tree "8. Playlists/"
2. Playlists/
├── Evening/...
└── Shower/...
Expand Down Expand Up @@ -227,7 +227,7 @@ BLACKPINK - 2016. SQUARE TWO - Single [Dance-Pop;K-Pop]
[18:20:53] INFO: Refreshing the read cache for 1 collages
[18:20:53] INFO: Applying cache updates for collage Road Trip

$ tree "7. Collages/Road Trip"
$ tree "7. Collages/Road Trip/"
7. Collages/Road Trip/
├── 1. LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [Dance-Pop;Future Bass;K-Pop]/...
└── 2. BLACKPINK - 2016. SQUARE TWO - Single [Dance-Pop;K-Pop]/...
Expand All @@ -246,7 +246,7 @@ LOOΠΔ ODD EYE CIRCLE - Chaotic.opus
[18:22:42] INFO: Refreshing the read cache for 1 playlists
[18:22:42] INFO: Applying cache updates for playlist Shower

$ tree "8. Playlists/Shower"
$ tree "8. Playlists/Shower/"
8. Playlists/Shower/
├── 1. BLACKPINK - PLAYING WITH FIRE.opus
└── 2. LOOΠΔ ODD EYE CIRCLE - Chaotic.opus
Expand Down Expand Up @@ -279,14 +279,14 @@ $ cd $fuse_mount_dir

$ rmdir "7. Collages/Morning"

$ tree "7. Collages"
7. Collages
$ tree "7. Collages/"
7. Collages/
└── Road Trip/...

$ rmdir "8. Playlists/Evening"

$ tree "8. Playlists"
8. Playlists
$ tree "8. Playlists/"
8. Playlists/
└── Shower/...
```

Expand All @@ -307,7 +307,7 @@ $ rose collages rename "Road Trip" "Long Flight"
[18:29:08] INFO: Evicting cached collages that are not on disk
[18:29:08] INFO: Evicted collage Road Trip from cache

$ tree "7. Collages"
$ tree "7. Collages/"
7. Collages/
└── Long Flight/...

Expand All @@ -318,7 +318,7 @@ $ rose playlists rename "Shower" "Meal Prep"
[18:30:17] INFO: Evicting cached playlists that are not on disk
[18:30:17] INFO: Evicted playlist Shower from cache

$ tree "8. Playlists"
$ tree "8. Playlists/"
8. Playlists/
└── Meal Prep/...
```
Expand All @@ -330,13 +330,55 @@ $ cd $fuse_mount_dir

$ mv "7. Collages/Road Trip/" "7. Collages/Long Flight"

$ tree "7. Collages"
7. Collages
$ tree "7. Collages/"
7. Collages/
└── Long Flight/...

$ mv "8. Playlists/Shower" "8. Playlsits/Meal Prep"

$ tree "8. Playlists"
8. Playlists
$ tree "8. Playlists/"
8. Playlists/
└── Meal Prep/...
```

## Set Playlist Cover Art

_This operation is playlist-only, as collages do not have their own cover art._

_The filename of the cover art in the virtual filesystem will always appear as
`cover.{ext}`, regardless of the cover art name in the source directory._

Command line:

```bash
$ cd $fuse_mount_dir

$ rose playlists set-cover "Shower" ./cover.jpg
[20:51:59] INFO: Set the cover of playlist Shower to cover.jpg
[20:51:59] INFO: Refreshing the read cache for 1 playlists
[20:51:59] INFO: Applying cache updates for playlist Shower

$ tree "8. Playlists/Shower/"
8. Playlists/Shower/
├── 1. BLACKPINK - PLAYING WITH FIRE.opus
├── 2. LOOΠΔ ODD EYE CIRCLE - Chaotic.opus
└── cover.jpg
```

Virtual filesystem:

_The filename of the created file in the release directory must be one of the
valid cover art filenames. The valid cover art filenames are controlled by and
documented in [Configuration](./CONFIGURATION.md)._

```bash
$ cd $fuse_mount_dir

$ mv ~/downloads/cover.jpg "8. Playlists/Shower/cover.jpg"

$ tree "8. Playlists/Shower/"
8. Playlists/Shower/
├── 1. BLACKPINK - PLAYING WITH FIRE.opus
├── 2. LOOΠΔ ODD EYE CIRCLE - Chaotic.opus
└── cover.jpg
```
48 changes: 47 additions & 1 deletion docs/VIRTUAL_FILESYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ All command line commands accept releases in three formats:
3. The path to the release in the virtual filesystem. The virtual filesystem
must be mounted for this format to work.

## Toggle NEW-ness
## Toggle Release NEW-ness

Command line:

Expand Down Expand Up @@ -152,6 +152,52 @@ $ tree "2. Releases - New/"
└── {NEW} LOOΠΔ - 2017. Kim Lip - Single [Contemporary R&B;Dance-Pop;K-Pop]/...
```

## Set Release Cover Art

_The filename of the cover art in the virtual filesystem will always appear as
`cover.{ext}`, regardless of the cover art name in the source directory._

Command line:

```bash
$ cd $fuse_mount_dir

$ rose releases set-cover "LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [Dance-Pop;Future Bass;K-Pop]" ./cover.jpg
[20:43:50] INFO: Set the cover of release LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match to cover.jpg
[20:43:50] INFO: Refreshing the read cache for 1 releases
[20:43:50] INFO: Applying cache updates for release LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match

$ tree "1. Releases/LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [Dance-Pop;Future Bass;K-Pop]/"
1. Releases/LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop]/
├── 01. LOOΠΔ ODD EYE CIRCLE - ODD.opus
├── 02. LOOΠΔ ODD EYE CIRCLE - Girl Front.opus
├── 03. LOOΠΔ ODD EYE CIRCLE - LOONATIC.opus
├── 04. LOOΠΔ ODD EYE CIRCLE - Chaotic.opus
├── 05. LOOΠΔ ODD EYE CIRCLE - Starlight.opus
└── cover.jpg
```

Virtual filesystem:

_The filename of the created file in the release directory must be one of the
valid cover art filenames. The valid cover art filenames are controlled by and
documented in [Configuration](./CONFIGURATION.md)._

```bash
$ cd $fuse_mount_dir

$ mv ~/downloads/cover.jpg "1. Releases/LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [Dance-Pop;Future Bass;K-Pop]/cover.jpg"

$ tree "1. Releases/LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [Dance-Pop;Future Bass;K-Pop]/"
1. Releases/LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop]/
├── 01. LOOΠΔ ODD EYE CIRCLE - ODD.opus
├── 02. LOOΠΔ ODD EYE CIRCLE - Girl Front.opus
├── 03. LOOΠΔ ODD EYE CIRCLE - LOONATIC.opus
├── 04. LOOΠΔ ODD EYE CIRCLE - Chaotic.opus
├── 05. LOOΠΔ ODD EYE CIRCLE - Starlight.opus
└── cover.jpg
```

## Delete a Release

_Deletion will move the release into the trashbin, following the
Expand Down
10 changes: 7 additions & 3 deletions rose/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from rose.artiststr import format_artist_string
from rose.common import VERSION
from rose.config import Config
from rose.tagger import SUPPORTED_EXTENSIONS, AudioFile
from rose.tagger import SUPPORTED_AUDIO_EXTENSIONS, AudioFile

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -572,7 +572,9 @@ def _update_cache_for_releases_executor(
try:
first_audio_file = Path(
next(
f for f in files if any(f.lower().endswith(ext) for ext in SUPPORTED_EXTENSIONS)
f
for f in files
if any(f.lower().endswith(ext) for ext in SUPPORTED_AUDIO_EXTENSIONS)
)
)
except StopIteration:
Expand Down Expand Up @@ -727,7 +729,9 @@ def _update_cache_for_releases_executor(
# tags.
pulled_release_tags = False
for f in files:
if not any(os.path.basename(f).lower().endswith(ext) for ext in SUPPORTED_EXTENSIONS):
if not any(
os.path.basename(f).lower().endswith(ext) for ext in SUPPORTED_AUDIO_EXTENSIONS
):
continue

cached_track = cached_tracks.get(f, None)
Expand Down
33 changes: 32 additions & 1 deletion rose/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,15 @@
edit_playlist_in_editor,
remove_track_from_playlist,
rename_playlist,
set_playlist_cover_art,
)
from rose.releases import (
delete_release,
dump_releases,
edit_release,
set_release_cover_art,
toggle_release_new,
)
from rose.releases import delete_release, dump_releases, edit_release, toggle_release_new
from rose.virtualfs import VirtualPath, mount_virtualfs, unmount_virtualfs
from rose.watcher import start_watchdog

Expand Down Expand Up @@ -186,6 +193,19 @@ def toggle_new(ctx: Context, release: str) -> None:
toggle_release_new(ctx.config, release)


@releases.command()
@click.argument("release", type=str, nargs=1)
@click.argument("cover", type=click.Path(path_type=Path), nargs=1)
@click.pass_obj
def set_cover(ctx: Context, release: str, cover: Path) -> None:
"""
Set the cover art of a release. For the release argument, accepts a release UUID, virtual
directory name, or virtual filesystem path. For the cover argument, accept a path to the image.
"""
release = parse_release_from_potential_path(ctx.config, release)
set_release_cover_art(ctx.config, release, cover)


@releases.command(name="delete")
@click.argument("release", type=str, nargs=1)
@click.pass_obj
Expand Down Expand Up @@ -338,6 +358,17 @@ def print3(ctx: Context) -> None:
print(dump_playlists(ctx.config))


@playlists.command(name="set-cover")
@click.argument("playlist", type=str, nargs=1)
@click.argument("cover", type=click.Path(path_type=Path), nargs=1)
@click.pass_obj
def set_cover2(ctx: Context, playlist: str, cover: Path) -> None:
"""
Set the cover art of a playlist. Accepts a playlist name and a path to an image.
"""
set_playlist_cover_art(ctx.config, playlist, cover)


def parse_release_from_potential_path(c: Config, r: str) -> str:
"""
Support paths from the virtual filesystem as valid releases. By default, we accept virtual
Expand Down
4 changes: 4 additions & 0 deletions rose/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class RoseError(Exception):
pass


class InvalidCoverArtFileError(RoseError):
pass


def valid_uuid(x: str) -> bool:
try:
uuid.UUID(x)
Expand Down
27 changes: 26 additions & 1 deletion rose/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import logging
import shutil
from collections import Counter
from pathlib import Path
from typing import Any
Expand All @@ -22,7 +23,7 @@
update_cache_evict_nonexistent_playlists,
update_cache_for_playlists,
)
from rose.common import RoseError
from rose.common import InvalidCoverArtFileError, RoseError
from rose.config import Config

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -202,5 +203,29 @@ def edit_playlist_in_editor(c: Config, playlist_name: str) -> None:
update_cache_for_playlists(c, [playlist_name], force=True)


def set_playlist_cover_art(c: Config, playlist_name: str, new_cover_art_path: Path) -> None:
"""
This function removes all potential cover arts for the playlist, and then copies the file
file located at the passed in path to be the playlist's art file.
"""
suffix = new_cover_art_path.suffix.lower()
if suffix[1:] not in c.valid_art_exts:
raise InvalidCoverArtFileError(
f"File {new_cover_art_path.name}'s extension is not supported for cover images: "
"To change this, please read the configuration documentation"
)

path = playlist_path(c, playlist_name)
if not path.exists():
raise PlaylistDoesNotExistError(f"Playlist {playlist_name} does not exist")
for f in (c.music_source_dir / "!playlists").iterdir():
if f.stem == playlist_name and f.suffix[1:].lower() in c.valid_art_exts:
logger.debug(f"Deleting existing cover art {f.name} in playlists")
f.unlink()
shutil.copyfile(new_cover_art_path, path.with_suffix(suffix))
logger.info(f"Set the cover of playlist {playlist_name} to {new_cover_art_path.name}")
update_cache_for_playlists(c, [playlist_name])


def playlist_path(c: Config, name: str) -> Path:
return c.music_source_dir / "!playlists" / f"{name}.toml"
26 changes: 25 additions & 1 deletion rose/playlists_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest
import tomllib

from conftest import TEST_RELEASE_1
from conftest import TEST_PLAYLIST_1, TEST_RELEASE_1
from rose.cache import connect, update_cache
from rose.config import Config
from rose.playlists import (
Expand All @@ -16,6 +16,7 @@
edit_playlist_in_editor,
remove_track_from_playlist,
rename_playlist,
set_playlist_cover_art,
)


Expand Down Expand Up @@ -234,3 +235,26 @@ def test_playlist_handle_missing_track(config: Config, source_dir: Path) -> None
with connect(config) as conn:
cursor = conn.execute("SELECT EXISTS(SELECT * FROM playlists WHERE name = 'You & Me')")
assert not cursor.fetchone()[0]


def test_set_playlist_cover_art(isolated_dir: Path, config: Config) -> None:
imagepath = isolated_dir / "folder.png"
with imagepath.open("w") as fp:
fp.write("lalala")

playlists_dir = config.music_source_dir / "!playlists"
shutil.copytree(TEST_PLAYLIST_1, playlists_dir)
(playlists_dir / "Turtle Rabbit.toml").touch()
(playlists_dir / "Turtle Rabbit.jpg").touch()
(playlists_dir / "Lala Lisa.txt").touch()
update_cache(config)

set_playlist_cover_art(config, "Lala Lisa", imagepath)
assert (playlists_dir / "Lala Lisa.png").is_file()
assert not (playlists_dir / "Lala Lisa.jpg").exists()
assert (playlists_dir / "Lala Lisa.txt").is_file()
assert len(list(playlists_dir.iterdir())) == 5

with connect(config) as conn:
cursor = conn.execute("SELECT cover_path FROM playlists WHERE name = 'Lala Lisa'")
assert Path(cursor.fetchone()["cover_path"]) == playlists_dir / "Lala Lisa.png"
Loading

0 comments on commit d5f3c25

Please sign in to comment.