Skip to content
This repository has been archived by the owner on Jun 15, 2024. It is now read-only.

Commit

Permalink
Refactor backend (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
vrslev authored Mar 20, 2023
1 parent 98deac9 commit d7c079c
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 115 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ dist
__pycache__
.venv
.pytest_cache
.ruff_cache
.coverage
node_modules
9 changes: 5 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ repos:
hooks:
- id: black

- repo: https://github.com/pycqa/isort
rev: 5.12.0
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.257
hooks:
- id: isort
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.0.0-alpha.6"
rev: v3.0.0-alpha.6
hooks:
- id: prettier
4 changes: 2 additions & 2 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ format = "v{base}"
[tool.black]
target-version = ["py310"]

[tool.isort]
profile = "black"
[tool.ruff]
ignore = ["E501"]

[tool.coverage.run]
source = ["src/playbacker"]
Expand Down
7 changes: 4 additions & 3 deletions backend/src/playbacker/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from fastapi.middleware import Middleware
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles

from playbacker.app.routes import get_router
from playbacker.app.dependencies import set_dependencies
from playbacker.app.routes import router
from playbacker.config import Config, get_config_file_path
from playbacker.core.playback import Playback
from playbacker.core.player import Player
Expand All @@ -30,7 +30,8 @@ def get_app(config: Config):
],
on_shutdown=[lambda: player.stop()],
)
app.include_router(get_router(config, player))
set_dependencies(app=app, player=player, config=config)
app.include_router(router)

frontend = Path(__file__).parent.parent / "dist"
if frontend.exists():
Expand Down
40 changes: 40 additions & 0 deletions backend/src/playbacker/app/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from pathlib import Path
from typing import Annotated

from fastapi import Depends, FastAPI
from playbacker.config import Config, get_setlists_dir_path, get_songs_file_path
from playbacker.core.player import Player


def get_player() -> Player:
...


CurrentPlayer = Annotated[Player, Depends(get_player)]


def get_config() -> Config:
...


CurrentConfig = Annotated[Config, Depends(get_config)]


def get_setlist_dir_path_dep(config: CurrentConfig):
return get_setlists_dir_path(config.config_dir_path)


CurrentSetlistDir = Annotated[Path, Depends(get_setlist_dir_path_dep)]


def get_songs_file_path_dep(config: CurrentConfig):
return get_songs_file_path(config.config_dir_path)


CurrentSongsFile = Annotated[Path, Depends(get_songs_file_path_dep)]


def set_dependencies(app: FastAPI, player: Player, config: Config):
app.dependency_overrides[get_player] = lambda: player
app.dependency_overrides[get_config] = lambda: config
app.dependency_overrides[get_config] = lambda: config
185 changes: 102 additions & 83 deletions backend/src/playbacker/app/routes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from pathlib import Path
from typing import Any, TypeVar
from collections.abc import Callable

import watchfiles
import yaml
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from sse_starlette import EventSourceResponse

from playbacker.config import Config, get_setlists_dir_path, get_songs_file_path
from playbacker.app.dependencies import (
CurrentPlayer,
CurrentSetlistDir,
CurrentSongsFile,
)
from playbacker.core.player import Player
from playbacker.core.setlist import (
NoSongInStorageError,
Expand All @@ -16,13 +19,20 @@
)
from playbacker.core.song import load_songs
from playbacker.core.tempo import Tempo
from pydantic import BaseModel
from sse_starlette import EventSourceResponse

router = APIRouter()

def get_setlist_path_from_pretty_name(name: str, setlists_dir_path: Path) -> Path:
map = {prettify_setlist_stem(f.stem): f for f in setlists_dir_path.glob("*.yaml")}
if path := map.get(name):
return path
raise HTTPException(404, "no setlist with this name")
_RouteFunc = TypeVar("_RouteFunc", bound=Callable[..., Any])


def post(func: _RouteFunc) -> _RouteFunc:
return router.post(f"/{func.__name__}")(func)


def get(func: _RouteFunc) -> _RouteFunc:
return router.get(f"/{func.__name__}")(func)


class PlayerState(BaseModel):
Expand All @@ -34,77 +44,86 @@ def make(cls, player: Player):
return cls(playing=player.playing, guide_enabled=player.guide_enabled)


def get_router(config: Config, player: Player):
router = APIRouter()
setlists_dir_path = get_setlists_dir_path(config.config_dir_path)
songs_file_path = get_songs_file_path(config.config_dir_path)

@router.post("/getSetlists")
def _():
stems = [
prettify_setlist_stem(f.stem) for f in setlists_dir_path.glob("*.yaml")
]
stems.sort(reverse=True)
return stems

@router.post("/getSetlist")
def _(name: str) -> Setlist:
path = get_setlist_path_from_pretty_name(name, setlists_dir_path)

with songs_file_path.open() as f:
songs = load_songs(content=yaml.safe_load(f))

with path.open() as f:
content = yaml.safe_load(f)

try:
return load_setlist(name=name, content=content, songs=songs)
except NoSongInStorageError as err:
raise HTTPException(404, err.message)

@router.post("/togglePlaying")
def _(tempo: Tempo):
if player.playing:
player.pause()
else:
player.play(tempo)
return PlayerState.make(player)

@router.post("/toggleGuideEnabled")
def _():
player.guide_enabled = not player.guide_enabled
return PlayerState.make(player)

@router.post("/prepareForSwitch")
def _():
player.prepare_for_switch()
return PlayerState.make(player)

@router.post("/reset")
def _():
player.reset()
return PlayerState.make(player)

@router.get("/watchSetlists")
def _():
async def watch():
async for _ in watchfiles.awatch( # pyright: ignore[reportUnknownMemberType]
setlists_dir_path
):
yield True

return EventSourceResponse(watch())

@router.get("/watchSetlist")
def _(name: str) -> EventSourceResponse:
path = get_setlist_path_from_pretty_name(name, setlists_dir_path)

async def watch():
async for _ in watchfiles.awatch( # pyright: ignore[reportUnknownMemberType]
path, songs_file_path
):
yield True

return EventSourceResponse(watch())

return router
@post
def get_setlists(setlists_dir: CurrentSetlistDir):
stems = [prettify_setlist_stem(f.stem) for f in setlists_dir.glob("*.yaml")]
stems.sort(reverse=True)
return stems


def get_setlist_path_from_pretty_name(name: str, setlists_dir_path: Path) -> Path:
map = {prettify_setlist_stem(f.stem): f for f in setlists_dir_path.glob("*.yaml")}
if path := map.get(name):
return path
raise HTTPException(404, "no setlist with this name")


@post
def get_setlist(
name: str, setlists_dir: CurrentSetlistDir, songs_file: CurrentSongsFile
) -> Setlist:
path = get_setlist_path_from_pretty_name(name, setlists_dir)

with songs_file.open() as f:
songs = load_songs(content=yaml.safe_load(f))

with path.open() as f:
content = yaml.safe_load(f)

try:
return load_setlist(name=name, content=content, songs=songs)
except NoSongInStorageError as err:
raise HTTPException(404, err.message)


@post
def toggle_playing(tempo: Tempo, player: CurrentPlayer):
if player.playing:
player.pause()
else:
player.play(tempo)
return PlayerState.make(player)


@post
def toggle_guide_enabled(player: CurrentPlayer):
player.guide_enabled = not player.guide_enabled
return PlayerState.make(player)


@post
def prepare_for_switch(player: CurrentPlayer):
player.prepare_for_switch()
return PlayerState.make(player)


@post
def reset(player: CurrentPlayer):
player.reset()
return PlayerState.make(player)


@get
def watch_setlists(setlist_dir: CurrentSetlistDir):
async def watch():
async for _ in watchfiles.awatch( # pyright: ignore[reportUnknownMemberType]
setlist_dir
):
yield True

return EventSourceResponse(watch())


@get
def watch_setlist(
name: str, setlist_dir: CurrentSetlistDir, songs_file: CurrentSongsFile
) -> EventSourceResponse:
path = get_setlist_path_from_pretty_name(name, setlist_dir)

async def watch():
async for _ in watchfiles.awatch( # pyright: ignore[reportUnknownMemberType]
path, songs_file
):
yield True

return EventSourceResponse(watch())
9 changes: 6 additions & 3 deletions backend/tests/audiofile_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from collections.abc import Callable
from pathlib import Path
from typing import Any
from unittest.mock import Mock
Expand All @@ -18,13 +17,17 @@ def audiofile():
def test_audiofile_data_no_resample(
audiofile: AudioFile, monkeypatch: pytest.MonkeyPatch
):
func: Callable[..., Any] = lambda _: ("mydata", 44100)
def func(_: Any):
return ("mydata", 44100)

monkeypatch.setattr(soundfile, "read", func)
assert audiofile.data == "mydata"


def test_audiofile_data_resample(audiofile: AudioFile, monkeypatch: pytest.MonkeyPatch):
func: Callable[..., Any] = lambda _: ("mydata", 48000)
def func(_: Any):
return "mydata", 48000

monkeypatch.setattr(soundfile, "read", func)

mock = Mock()
Expand Down
4 changes: 3 additions & 1 deletion backend/tests/settings_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def query_devices(device: str | None, kind: str):

@pytest.fixture
def devices():
m = lambda: _ChannelMap(metronome=[1], guide=[2], multitrack=[3, 4])
def m():
return _ChannelMap(metronome=[1], guide=[2], multitrack=[3, 4])

return [
_Device(
name="dev1", pretty_name="Device 1", sample_rate=48000, channel_map=m()
Expand Down
24 changes: 12 additions & 12 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export interface PlayerState {
}

export interface Player {
getSetlists(): Promise<string[]>;
getSetlist(name: string): Promise<Setlist>;
togglePlaying(tempo: Tempo): Promise<PlayerState>;
toggleGuideEnabled(): Promise<PlayerState>;
prepareForSwitch(): Promise<PlayerState>;
get_setlists(): Promise<string[]>;
get_setlist(name: string): Promise<Setlist>;
toggle_playing(tempo: Tempo): Promise<PlayerState>;
toggle_guide_enabled(): Promise<PlayerState>;
prepare_for_switch(): Promise<PlayerState>;
reset(): Promise<PlayerState>;
}

Expand All @@ -36,16 +36,16 @@ export function apiPlayer(): Player {
(await fetch(makeUrl(path), { method: "POST", ...init })).json();

return {
getSetlists: () => e("/getSetlists"),
getSetlist: (name: string) =>
e(`/getSetlist?${new URLSearchParams({ name })}`),
togglePlaying: (tempo: Tempo) =>
e("/togglePlaying", {
get_setlists: () => e("/get_setlists"),
get_setlist: (name: string) =>
e(`/get_setlist?${new URLSearchParams({ name })}`),
toggle_playing: (tempo: Tempo) =>
e("/toggle_playing", {
headers: { "Content-Type": "application/json" },
body: JSON.stringify(tempo),
}),
toggleGuideEnabled: () => e("/toggleGuideEnabled"),
prepareForSwitch: () => e("/prepareForSwitch"),
toggle_guide_enabled: () => e("/toggle_guide_enabled"),
prepare_for_switch: () => e("/prepare_for_switch"),
reset: () => e("/reset"),
};
}
Loading

0 comments on commit d7c079c

Please sign in to comment.