Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ jobs:
python: "3.14"
tox: "3.14"
coverage: true
# - name: "pyright"
# python: "3.14"
# tox: "pyright"
- name: "pyright"
python: "3.14"
tox: "pyright"
- name: "ruff check"
python: "3.14"
tox: "ruff-check"
Expand Down
12 changes: 2 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ classifiers = [
]
dynamic = ["version"]
dependencies = [
"mopidy >= 4.0.0a7",
"mopidy >= 4.0.0rc2",
"pykka >= 4.1",
"requests >= 2.32",
]
Expand Down Expand Up @@ -86,16 +86,7 @@ target-version = "py313"
select = ["ALL"]
ignore = [
# Add rules you want to ignore here
"ANN001", # missing-type-function-argument # TODO
"ANN002", # missing-type-args # TODO
"ANN003", # missing-type-kwargs # TODO
"ANN201", # missing-return-type-undocumented-public-function # TODO
"ANN202", # missing-return-type-private-function # TODO
"ANN204", # missing-return-type-special-method # TODO
"ANN205", # missing-return-type-static-method # TODO
"D", # pydocstyle
"D203", # one-blank-line-before-class
"D213", # multi-line-summary-second-line
"FIX002", # line-contains-todo # TODO
"G004", # logging-f-string
"TD002", # missing-todo-author
Expand All @@ -108,6 +99,7 @@ ignore = [

[tool.ruff.lint.per-file-ignores]
"tests/*" = [
"ANN", # flake8-annotations
"ARG", # flake8-unused-arguments
"D", # pydocstyle
"S101", # assert
Expand Down
6 changes: 3 additions & 3 deletions src/mopidy_beets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ class Extension(ext.Extension):
ext_name = "beets"
version = __version__

def get_default_config(self):
def get_default_config(self) -> str:
return config.read(pathlib.Path(__file__).parent / "ext.conf")

def get_config_schema(self):
def get_config_schema(self) -> config.ConfigSchema:
schema = super().get_config_schema()
schema["hostname"] = config.Hostname()
schema["port"] = config.Port()
return schema

def setup(self, registry):
def setup(self, registry: ext.Registry) -> None:
from mopidy_beets.actor import BeetsBackend # noqa: PLC0415

registry.add("backend", BeetsBackend)
18 changes: 13 additions & 5 deletions src/mopidy_beets/actor.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
from __future__ import annotations

import logging
from typing import ClassVar
from typing import TYPE_CHECKING, ClassVar, override

import pykka
from mopidy import backend
from mopidy.types import UriScheme
from mopidy.types import Uri, UriScheme

from .client import BeetsRemoteClient
from .library import BeetsLibraryProvider

if TYPE_CHECKING:
from mopidy.audio import AudioProxy
from mopidy.config import Config

logger = logging.getLogger(__name__)


class BeetsBackend(pykka.ThreadingActor, backend.Backend):
uri_schemes: ClassVar[list[UriScheme]] = [UriScheme("beets")]

def __init__(self, config, audio):
super().__init__()
@override
def __init__(self, *, config: Config, audio: AudioProxy) -> None:
super().__init__(config=config, audio=audio)

beets_endpoint = (
f"http://{config['beets']['hostname']}:{config['beets']['port']}"
Expand All @@ -30,7 +37,8 @@ def __init__(self, config, audio):
class BeetsPlaybackProvider(backend.PlaybackProvider):
backend: BeetsBackend

def translate_uri(self, uri):
@override
def translate_uri(self, uri: Uri) -> Uri | None:
track_id = uri.split(";")[1]
logger.debug(f"Getting info for track {uri} with id {track_id}")
return self.backend.beets_api.get_track_stream_url(track_id)
16 changes: 13 additions & 3 deletions src/mopidy_beets/browsers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from mopidy.models import Ref

from mopidy_beets.client import BeetsRemoteClient


class GenericBrowserBase:
def __init__(self, ref, api):
def __init__(self, ref: Ref, api: BeetsRemoteClient) -> None:
self.ref = ref
self.api = api

def get_toplevel(self):
def get_toplevel(self) -> list[Ref]:
"""deliver the top level directories or tracks for this browser

The result is a list of ``mopidy.models.Ref`` objects.
Usually this list contains entries like "genre" or other categories.
"""
raise NotImplementedError

def get_directory(self, key):
def get_directory(self, key: str) -> list[Ref]:
"""deliver the corresponding sub items for a given category key

The result is a list of ``mopidy.models.Ref`` objects.
Expand Down
50 changes: 32 additions & 18 deletions src/mopidy_beets/browsers/albums.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,62 @@
from mopidy import models
from __future__ import annotations

from typing import ClassVar, override

from mopidy.models import Album, Ref

from mopidy_beets.browsers import GenericBrowserBase
from mopidy_beets.translator import assemble_uri


class AlbumsCategoryBrowser(GenericBrowserBase):
field = None
sort_fields = None
label_fields = None
field: ClassVar[str]
sort_fields: ClassVar[tuple[str, ...]]

def get_toplevel(self):
def get_toplevel(self) -> list[Ref]:
keys = self.api.get_sorted_unique_album_attributes(self.field)
return [
models.Ref.directory(
name=str(key), uri=assemble_uri(self.ref.uri, id_value=key)
Ref.directory(
name=str(k),
uri=assemble_uri(self.ref.uri, id_value=k),
)
for key in keys
for k in sorted(keys)
]

def get_directory(self, key):
def get_directory(self, key: str) -> list[Ref]:
albums = self.api.get_albums_by(
[(self.field, key)],
True, # noqa: FBT003
self.sort_fields,
exact_text=True,
sort_fields=self.sort_fields,
)
return [
models.Ref.album(uri=album.uri, name=self._get_label(album))
for album in albums
Ref.album(
uri=a.uri,
name=self._get_label(a),
)
for a in albums
if a.uri is not None
]

def _get_label(self, album: Album) -> str | None:
raise NotImplementedError


class AlbumsByArtistBrowser(AlbumsCategoryBrowser):
field = "albumartist"
sort_fields = ("original_year+", "year+", "album+")

def _get_label(self, album):
@override
def _get_label(self, album: Album) -> str | None:
return album.name


class AlbumsByGenreBrowser(AlbumsCategoryBrowser):
field = "genre"
sort_fields = ("albumartist", "original_year+", "year+", "album+")

def _get_label(self, album):
artists = " / ".join([artist.name for artist in album.artists])
@override
def _get_label(self, album: Album) -> str | None:
artists = " / ".join([a.name for a in album.artists if a.name is not None])
if artists and album.date:
return f"{artists} - {album.name} ({album.date.split('-')[0]})"
if artists:
Expand All @@ -61,8 +74,9 @@ class AlbumsByYearBrowser(AlbumsCategoryBrowser):
"album+",
)

def _get_label(self, album):
artists = " / ".join([artist.name for artist in album.artists])
@override
def _get_label(self, album: Album) -> str | None:
artists = " / ".join([a.name for a in album.artists if a.name is not None])
if artists:
return f"{artists} - {album.name}"
return album.name
Loading
Loading