Skip to content

Commit

Permalink
implement watchdog
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline committed Oct 18, 2023
1 parent 73c3c79 commit 04b273a
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 78 deletions.
19 changes: 18 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hashlib
import logging
import shutil
import sqlite3
from collections.abc import Iterator
from pathlib import Path
Expand All @@ -8,11 +9,17 @@
import pytest
from click.testing import CliRunner

from rose.cache import CACHE_SCHEMA_PATH
from rose.cache import CACHE_SCHEMA_PATH, update_cache
from rose.config import Config

logger = logging.getLogger(__name__)

TESTDATA = Path(__file__).resolve().parent / "testdata" / "cache"
TEST_RELEASE_1 = TESTDATA / "Test Release 1"
TEST_RELEASE_2 = TESTDATA / "Test Release 2"
TEST_RELEASE_3 = TESTDATA / "Test Release 3"
TEST_COLLAGE_1 = TESTDATA / "Collage 1"


@pytest.fixture(autouse=True)
def debug_logging() -> None:
Expand Down Expand Up @@ -130,6 +137,16 @@ def seeded_cache(config: Config) -> None:
f.touch()


@pytest.fixture()
def source_dir(config: Config) -> Path:
shutil.copytree(TEST_RELEASE_1, config.music_source_dir / TEST_RELEASE_1.name)
shutil.copytree(TEST_RELEASE_2, config.music_source_dir / TEST_RELEASE_2.name)
shutil.copytree(TEST_RELEASE_3, config.music_source_dir / TEST_RELEASE_3.name)
shutil.copytree(TEST_COLLAGE_1, config.music_source_dir / "!collages")
update_cache(config)
return config.music_source_dir


def freeze_database_time(conn: sqlite3.Connection) -> None:
"""
This function freezes the CURRENT_TIMESTAMP function in SQLite3 to
Expand Down
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
setuptools
tomli-w
uuid6-python
watchdog
];
dev-deps = with python.pkgs; [
black
Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ ignore = [
]
line-length = 100
exclude = [".venv"]
unfixable = [
# Remove unused variables.
"F841",
]
src = ["."]

[tool.mypy]
Expand All @@ -71,6 +75,9 @@ ignore_missing_imports = true
module = "send2trash"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "watchdog.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "setuptools"
ignore_missing_imports = true

Expand Down
9 changes: 9 additions & 0 deletions rose/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from rose.config import Config
from rose.releases import dump_releases
from rose.virtualfs import mount_virtualfs, unmount_virtualfs
from rose.watcher import start_watchdog


@dataclass
Expand Down Expand Up @@ -56,6 +57,14 @@ def update(ctx: Context, force: bool) -> None:
update_cache(ctx.config, force)


@cache.command()
@click.option("--foreground", "-f", is_flag=True, help="Foreground the cache watcher.")
@click.pass_obj
def watch(ctx: Context, foreground: bool) -> None:
"""Start a watchdog that will auto-refresh the cache on changes in music_source_dir."""
start_watchdog(ctx.config, foreground)


@cli.group()
def fs() -> None:
"""Manage the virtual library."""
Expand Down
6 changes: 6 additions & 0 deletions rose/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ def update_cache_for_releases(
for rd in release_dirs:
release_id = None
files: list[os.DirEntry[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]
Expand Down Expand Up @@ -837,6 +840,9 @@ def update_cache_for_collages(
path = Path(f.path)
if path.suffix != ".toml":
continue
if not path.is_file():
logger.debug(f"Skipping processing collage {path.name} because it is not a file")
continue
if collage_names is None or path.stem in collage_names:
files.append((path.resolve(), path.stem, f))
logger.info(f"Refreshing the read cache for {len(files)} collages")
Expand Down
8 changes: 1 addition & 7 deletions rose/cache_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
import tomllib

from conftest import TEST_COLLAGE_1, TEST_RELEASE_1, TEST_RELEASE_2
from rose.cache import (
CACHE_SCHEMA_PATH,
STORED_DATA_FILE_REGEX,
Expand Down Expand Up @@ -62,13 +63,6 @@ def test_migration(config: Config) -> None:
assert cursor.fetchone()[0] == 1


TESTDATA = Path(__file__).resolve().parent.parent / "testdata" / "cache"
TEST_RELEASE_1 = TESTDATA / "Test Release 1"
TEST_RELEASE_2 = TESTDATA / "Test Release 2"
TEST_RELEASE_3 = TESTDATA / "Test Release 3"
TEST_COLLAGE_1 = TESTDATA / "Collage 1"


def test_update_cache_all(config: Config) -> None:
"""Test that the update all function works."""
shutil.copytree(TEST_RELEASE_1, config.music_source_dir / TEST_RELEASE_1.name)
Expand Down
102 changes: 32 additions & 70 deletions rose/collages_test.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import shutil
from pathlib import Path
from typing import Any

import pytest
import tomllib

from rose.cache import connect, update_cache
from rose.cache_test import TEST_COLLAGE_1, TEST_RELEASE_2, TEST_RELEASE_3
from rose.cache import connect
from rose.collages import (
add_release_to_collage,
create_collage,
Expand All @@ -20,20 +19,13 @@
# TODO: Fixture for common setup.


def test_delete_release_from_collage(config: Config) -> None:
# Set up the filesystem that will be updated.
shutil.copytree(TEST_RELEASE_2, config.music_source_dir / TEST_RELEASE_2.name)
shutil.copytree(TEST_RELEASE_3, config.music_source_dir / TEST_RELEASE_3.name)
shutil.copytree(TEST_COLLAGE_1, config.music_source_dir / "!collages")
# Bootstrap initial cache.
update_cache(config)

def test_delete_release_from_collage(config: Config, source_dir: Path) -> None:
delete_release_from_collage(
config, "Rose Gold", "Carly Rae Jepsen - 1990. I Love Carly [Pop;Dream Pop] {A Cool Label}"
)

# Assert file is updated.
with (config.music_source_dir / "!collages" / "Rose Gold.toml").open("rb") as fp:
with (source_dir / "!collages" / "Rose Gold.toml").open("rb") as fp:
diskdata = tomllib.load(fp)
assert len(diskdata["releases"]) == 1
assert diskdata["releases"][0]["uuid"] == "ilovenewjeans"
Expand All @@ -47,94 +39,84 @@ def test_delete_release_from_collage(config: Config) -> None:
assert ids == ["ilovenewjeans"]


def test_collage_lifecycle(config: Config) -> None:
# Set up the filesystem that will be updated.
shutil.copytree(TEST_RELEASE_2, config.music_source_dir / TEST_RELEASE_2.name)
shutil.copytree(TEST_RELEASE_3, config.music_source_dir / TEST_RELEASE_3.name)
# Bootstrap initial cache.
update_cache(config)

filepath = config.music_source_dir / "!collages" / "Rose Gold.toml"
def test_collage_lifecycle(config: Config, source_dir: Path) -> None:
filepath = source_dir / "!collages" / "All Eyes.toml"

# Create collage.
assert not filepath.exists()
create_collage(config, "Rose Gold")
create_collage(config, "All Eyes")
assert filepath.is_file()
with connect(config) as conn:
cursor = conn.execute("SELECT EXISTS(SELECT * FROM collages WHERE name = 'Rose Gold')")
cursor = conn.execute("SELECT EXISTS(SELECT * FROM collages WHERE name = 'All Eyes')")
assert cursor.fetchone()[0]

# Add one release.
add_release_to_collage(
config, "Rose Gold", "Carly Rae Jepsen - 1990. I Love Carly [Pop;Dream Pop] {A Cool Label}"
config, "All Eyes", "Carly Rae Jepsen - 1990. I Love Carly [Pop;Dream Pop] {A Cool Label}"
)
with filepath.open("rb") as fp:
diskdata = tomllib.load(fp)
assert {r["uuid"] for r in diskdata["releases"]} == {"ilovecarly"}
with connect(config) as conn:
cursor = conn.execute(
"SELECT release_id FROM collages_releases WHERE collage_name = 'Rose Gold'"
"SELECT release_id FROM collages_releases WHERE collage_name = 'All Eyes'"
)
assert {r["release_id"] for r in cursor} == {"ilovecarly"}

# Add another release.
add_release_to_collage(
config, "Rose Gold", "NewJeans - 1990. I Love NewJeans [K-Pop;R&B] {A Cool Label}"
config, "All Eyes", "NewJeans - 1990. I Love NewJeans [K-Pop;R&B] {A Cool Label}"
)
with (config.music_source_dir / "!collages" / "Rose Gold.toml").open("rb") as fp:
with (source_dir / "!collages" / "All Eyes.toml").open("rb") as fp:
diskdata = tomllib.load(fp)
assert {r["uuid"] for r in diskdata["releases"]} == {"ilovecarly", "ilovenewjeans"}
with connect(config) as conn:
cursor = conn.execute(
"SELECT release_id FROM collages_releases WHERE collage_name = 'Rose Gold'"
"SELECT release_id FROM collages_releases WHERE collage_name = 'All Eyes'"
)
assert {r["release_id"] for r in cursor} == {"ilovecarly", "ilovenewjeans"}

# Delete one release.
delete_release_from_collage(
config, "Rose Gold", "NewJeans - 1990. I Love NewJeans [K-Pop;R&B] {A Cool Label}"
config, "All Eyes", "NewJeans - 1990. I Love NewJeans [K-Pop;R&B] {A Cool Label}"
)
with filepath.open("rb") as fp:
diskdata = tomllib.load(fp)
assert {r["uuid"] for r in diskdata["releases"]} == {"ilovecarly"}
with connect(config) as conn:
cursor = conn.execute(
"SELECT release_id FROM collages_releases WHERE collage_name = 'Rose Gold'"
"SELECT release_id FROM collages_releases WHERE collage_name = 'All Eyes'"
)
assert {r["release_id"] for r in cursor} == {"ilovecarly"}

# And delete the collage.
delete_collage(config, "Rose Gold")
delete_collage(config, "All Eyes")
assert not filepath.is_file()
with connect(config) as conn:
cursor = conn.execute("SELECT EXISTS(SELECT * FROM collages WHERE name = 'Rose Gold')")
cursor = conn.execute("SELECT EXISTS(SELECT * FROM collages WHERE name = 'All Eyes')")
assert not cursor.fetchone()[0]


def test_collage_add_duplicate(config: Config) -> None:
shutil.copytree(TEST_RELEASE_3, config.music_source_dir / TEST_RELEASE_3.name)
update_cache(config)
create_collage(config, "Rose Gold")
def test_collage_add_duplicate(config: Config, source_dir: Path) -> None:
create_collage(config, "All Eyes")
add_release_to_collage(
config, "Rose Gold", "NewJeans - 1990. I Love NewJeans [K-Pop;R&B] {A Cool Label}"
config, "All Eyes", "NewJeans - 1990. I Love NewJeans [K-Pop;R&B] {A Cool Label}"
)
add_release_to_collage(
config, "Rose Gold", "NewJeans - 1990. I Love NewJeans [K-Pop;R&B] {A Cool Label}"
config, "All Eyes", "NewJeans - 1990. I Love NewJeans [K-Pop;R&B] {A Cool Label}"
)
with (config.music_source_dir / "!collages" / "Rose Gold.toml").open("rb") as fp:
with (source_dir / "!collages" / "All Eyes.toml").open("rb") as fp:
diskdata = tomllib.load(fp)
assert len(diskdata["releases"]) == 1
with connect(config) as conn:
cursor = conn.execute("SELECT * FROM collages_releases WHERE collage_name = 'Rose Gold'")
cursor = conn.execute("SELECT * FROM collages_releases WHERE collage_name = 'All Eyes'")
assert len(cursor.fetchall()) == 1


def test_rename_collage(config: Config) -> None:
shutil.copytree(TEST_COLLAGE_1, config.music_source_dir / "!collages")
def test_rename_collage(config: Config, source_dir: Path) -> None:
rename_collage(config, "Rose Gold", "Black Pink")

assert not (config.music_source_dir / "!collages" / "Rose Gold.toml").exists()
assert (config.music_source_dir / "!collages" / "Black Pink.toml").exists()
assert not (source_dir / "!collages" / "Rose Gold.toml").exists()
assert (source_dir / "!collages" / "Black Pink.toml").exists()
with connect(config) as conn:
cursor = conn.execute("SELECT EXISTS(SELECT * FROM collages WHERE name = 'Black Pink')")
assert cursor.fetchone()[0]
Expand All @@ -150,19 +132,9 @@ def test_dump_collages(config: Config) -> None:
# fmt: on


def test_edit_collages_ordering(monkeypatch: Any, config: Config) -> None:
# Set up the filesystem that will be updated.
shutil.copytree(TEST_RELEASE_2, config.music_source_dir / TEST_RELEASE_2.name)
shutil.copytree(TEST_RELEASE_3, config.music_source_dir / TEST_RELEASE_3.name)
shutil.copytree(TEST_COLLAGE_1, config.music_source_dir / "!collages")
# Bootstrap initial cache.
update_cache(config)
filepath = config.music_source_dir / "!collages" / "Rose Gold.toml"

def mock_edit(x: str) -> str:
return "\n".join(reversed(x.split("\n")))

monkeypatch.setattr("rose.collages.click.edit", mock_edit)
def test_edit_collages_ordering(monkeypatch: Any, config: Config, source_dir: Path) -> None:
filepath = source_dir / "!collages" / "Rose Gold.toml"
monkeypatch.setattr("rose.collages.click.edit", lambda x: "\n".join(reversed(x.split("\n"))))
edit_collage_in_editor(config, "Rose Gold")

with filepath.open("rb") as fp:
Expand All @@ -171,19 +143,9 @@ def mock_edit(x: str) -> str:
assert data["releases"][1]["uuid"] == "ilovecarly"


def test_edit_collages_delete_release(monkeypatch: Any, config: Config) -> None:
# Set up the filesystem that will be updated.
shutil.copytree(TEST_RELEASE_2, config.music_source_dir / TEST_RELEASE_2.name)
shutil.copytree(TEST_RELEASE_3, config.music_source_dir / TEST_RELEASE_3.name)
shutil.copytree(TEST_COLLAGE_1, config.music_source_dir / "!collages")
# Bootstrap initial cache.
update_cache(config)
filepath = config.music_source_dir / "!collages" / "Rose Gold.toml"

def mock_edit(x: str) -> str:
return x.split("\n")[0]

monkeypatch.setattr("rose.collages.click.edit", mock_edit)
def test_edit_collages_delete_release(monkeypatch: Any, config: Config, source_dir: Path) -> None:
filepath = source_dir / "!collages" / "Rose Gold.toml"
monkeypatch.setattr("rose.collages.click.edit", lambda x: x.split("\n")[0])
edit_collage_in_editor(config, "Rose Gold")

with filepath.open("rb") as fp:
Expand Down
Loading

0 comments on commit 04b273a

Please sign in to comment.