[deps] Fix so it works with older mopidy
Some checks failed
Some checks failed
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
name = "mopidy-smartplaylists"
|
||||
description = "Mopidy extension for generating smart playlists based on track properties"
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.13"
|
||||
requires-python = ">= 3.10"
|
||||
license = { text = "Apache-2.0" }
|
||||
authors = [{ name = "Chris Powell", email = "chris@code.unbl.ink" }]
|
||||
classifiers = [
|
||||
@ -14,9 +14,9 @@ classifiers = [
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"mopidy >= 4.0.0",
|
||||
"pykka >= 4.1",
|
||||
"tornado >= 6.4",
|
||||
"mopidy >= 3.0.0",
|
||||
"pykka >= 2.0.1",
|
||||
"tornado >= 4.4",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@ -54,7 +54,7 @@ show_missing = true
|
||||
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.13"
|
||||
pythonVersion = "3.10"
|
||||
typeCheckingMode = "standard"
|
||||
reportMissingTypeStubs = false
|
||||
reportPrivateImportUsage = false
|
||||
@ -67,7 +67,7 @@ filterwarnings = [
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["ALL"]
|
||||
@ -105,6 +105,9 @@ ignore = [
|
||||
|
||||
[tool.tox]
|
||||
env_list = [
|
||||
"3.10",
|
||||
"3.11",
|
||||
"3.12",
|
||||
"3.13",
|
||||
"3.14",
|
||||
"pyright",
|
||||
|
||||
@ -2,7 +2,6 @@ import pathlib
|
||||
from importlib.metadata import version
|
||||
|
||||
from mopidy import config, ext
|
||||
from mopidy.config import ConfigSchema
|
||||
|
||||
__version__ = version("mopidy-smartplaylists")
|
||||
|
||||
@ -12,10 +11,10 @@ class Extension(ext.Extension):
|
||||
ext_name = "smartplaylists"
|
||||
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["decades"] = config.String(optional=True)
|
||||
schema["genres"] = config.String(optional=True)
|
||||
@ -24,16 +23,19 @@ class Extension(ext.Extension):
|
||||
schema["refresh_interval"] = config.Integer(optional=True, minimum=0)
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
def validate_environment(self) -> None:
|
||||
pass
|
||||
|
||||
def setup(self, registry):
|
||||
def setup(self, registry: ext.Registry) -> None:
|
||||
from mopidy_smartplaylists.web import app_factory
|
||||
|
||||
registry.add("http:app", {
|
||||
registry.add(
|
||||
"http:app",
|
||||
{
|
||||
"name": self.ext_name,
|
||||
"factory": app_factory,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
from mopidy_smartplaylists.frontend import SmartPlaylistsFrontend
|
||||
|
||||
|
||||
23
src/mopidy_smartplaylists/compat.py
Normal file
23
src/mopidy_smartplaylists/compat.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""
|
||||
Compatibility layer for Mopidy 3 vs 4 differences.
|
||||
|
||||
Provides import aliases for APIs that differ between Mopidy 3 and 4:
|
||||
|
||||
- ``CoreProxy`` / ``Config`` are available in both but under different modules.
|
||||
- ``mopidy.types`` (``Uri``, ``SearchField``, ``Query``) only exists in Mopidy 4.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mopidy.config import Config
|
||||
from mopidy.core import CoreProxy
|
||||
from mopidy.types import Query, SearchField, Uri
|
||||
else:
|
||||
CoreProxy = Any
|
||||
Config = dict[str, dict[str, Any]]
|
||||
Uri = str
|
||||
SearchField = str
|
||||
Query = dict
|
||||
@ -1,11 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pykka
|
||||
from mopidy.config import Config
|
||||
from mopidy.core import CoreListener, CoreProxy
|
||||
from mopidy.core import CoreListener
|
||||
|
||||
from mopidy_smartplaylists.generators import refresh_smart_playlists
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mopidy_smartplaylists.compat import Config, CoreProxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@ -1,23 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from mopidy.core import CoreProxy
|
||||
from mopidy.models import Playlist, Track, TrackField
|
||||
from mopidy.models import Playlist, Track
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mopidy_smartplaylists.compat import CoreProxy, Query, SearchField, Uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_decade(decade_str: str) -> str:
|
||||
decade_str = decade_str.strip()
|
||||
if len(decade_str) == 4:
|
||||
return decade_str[:3] + "*"
|
||||
if decade_str.endswith("s"):
|
||||
decade_str = decade_str.strip().rstrip("s")
|
||||
if len(decade_str) >= 3:
|
||||
return decade_str[:3] + "*"
|
||||
return decade_str
|
||||
|
||||
|
||||
def _search(core: CoreProxy, field: str, value: str) -> list[Track]:
|
||||
query = cast("Query[SearchField]", {field: [value]})
|
||||
result = core.library.search(query).get()
|
||||
return _extract_tracks(result)
|
||||
|
||||
|
||||
def build_decade_mix(core: CoreProxy, decade: str) -> list[Track]:
|
||||
query = {TrackField.DATE: [parse_decade(decade)]}
|
||||
query = cast("Query[SearchField]", {"date": [parse_decade(decade)]})
|
||||
result = core.library.search(query).get()
|
||||
tracks = _extract_tracks(result)
|
||||
logger.info("Found %d tracks for decade %s", len(tracks), decade)
|
||||
@ -25,30 +34,22 @@ def build_decade_mix(core: CoreProxy, decade: str) -> list[Track]:
|
||||
|
||||
|
||||
def build_genre_mix(core: CoreProxy, genre: str) -> list[Track]:
|
||||
query = {TrackField.GENRE: [genre]}
|
||||
result = core.library.search(query).get()
|
||||
tracks = _extract_tracks(result)
|
||||
logger.info("Found %d tracks for genre %s", len(tracks), genre)
|
||||
return tracks
|
||||
return _search(core, "genre", genre)
|
||||
|
||||
|
||||
def build_artist_mix(core: CoreProxy, artist: str) -> list[Track]:
|
||||
query = {TrackField.ARTIST: [artist]}
|
||||
result = core.library.search(query).get()
|
||||
tracks = _extract_tracks(result)
|
||||
logger.info("Found %d tracks for artist %s", len(tracks), artist)
|
||||
return tracks
|
||||
return _search(core, "artist", artist)
|
||||
|
||||
|
||||
def build_album_mix(core: CoreProxy, album_uri: str) -> list[Track]:
|
||||
tracks = core.library.lookup([album_uri]).get()
|
||||
tracks = core.library.lookup(cast("list[Uri]", [album_uri])).get()
|
||||
flat = _flatten_lookup(tracks)
|
||||
logger.info("Found %d tracks for album %s", len(flat), album_uri)
|
||||
return flat
|
||||
|
||||
|
||||
def build_instant_mix(core: CoreProxy, track_uri: str, limit: int = 50) -> list[Track]:
|
||||
lookup_result = core.library.lookup([track_uri]).get()
|
||||
lookup_result = core.library.lookup(cast("list[Uri]", [track_uri])).get()
|
||||
seed_tracks = _flatten_lookup(lookup_result)
|
||||
if not seed_tracks:
|
||||
logger.warning("No track found for URI: %s", track_uri)
|
||||
@ -61,16 +62,14 @@ def build_instant_mix(core: CoreProxy, track_uri: str, limit: int = 50) -> list[
|
||||
if seed.genre:
|
||||
genres = [g.strip() for g in seed.genre.split("/") if g.strip()]
|
||||
if seed.artists:
|
||||
for artist_ref in seed.artists:
|
||||
if artist_ref.name:
|
||||
artists.append(artist_ref.name)
|
||||
artists.extend(a_ref.name for a_ref in seed.artists if a_ref.name)
|
||||
|
||||
similar: dict[str, Track] = {}
|
||||
|
||||
for genre in genres:
|
||||
query = {TrackField.GENRE: [genre]}
|
||||
result = core.library.search(query).get()
|
||||
for batch in result:
|
||||
g_query = cast("Query[SearchField]", {"genre": [genre]})
|
||||
genre_result = core.library.search(g_query).get()
|
||||
for batch in genre_result:
|
||||
for t in batch.tracks:
|
||||
if t.uri and t.uri != track_uri:
|
||||
similar[t.uri] = t
|
||||
@ -78,9 +77,9 @@ def build_instant_mix(core: CoreProxy, track_uri: str, limit: int = 50) -> list[
|
||||
for artist in artists:
|
||||
if len(similar) >= limit:
|
||||
break
|
||||
query = {TrackField.ARTIST: [artist]}
|
||||
result = core.library.search(query).get()
|
||||
for batch in result:
|
||||
a_query = cast("Query[SearchField]", {"artist": [artist]})
|
||||
artist_result = core.library.search(a_query).get()
|
||||
for batch in artist_result:
|
||||
for t in batch.tracks:
|
||||
if t.uri and t.uri != track_uri:
|
||||
similar[t.uri] = t
|
||||
@ -97,16 +96,16 @@ def save_smart_playlist(
|
||||
tracks: list[Track],
|
||||
) -> Playlist | None:
|
||||
playlist_name = f"{prefix} {name}"
|
||||
uri = f"mopidy:smartplaylists:{_sanitize_name(name)}"
|
||||
uri: str = f"mopidy:smartplaylists:{_sanitize_name(name)}"
|
||||
|
||||
existing = core.playlists.lookup(uri).get()
|
||||
existing = core.playlists.lookup(cast("Uri", uri)).get()
|
||||
if existing:
|
||||
core.playlists.delete(existing.uri).get()
|
||||
|
||||
playlist = Playlist(
|
||||
name=playlist_name,
|
||||
uri=uri,
|
||||
tracks=tracks,
|
||||
uri=cast("Uri", uri),
|
||||
tracks=tuple(tracks),
|
||||
)
|
||||
saved = core.playlists.save(playlist).get()
|
||||
logger.info("Saved smart playlist: %s (%d tracks)", playlist_name, len(tracks))
|
||||
@ -141,14 +140,14 @@ def refresh_smart_playlists(core: CoreProxy, config_dict: dict) -> None:
|
||||
save_smart_playlist(core, prefix, f"{artist} Mix", tracks)
|
||||
|
||||
|
||||
def _extract_tracks(search_result) -> list[Track]:
|
||||
def _extract_tracks(search_result: list) -> list[Track]:
|
||||
tracks: list[Track] = []
|
||||
for batch in search_result:
|
||||
tracks.extend(batch.tracks)
|
||||
return tracks
|
||||
|
||||
|
||||
def _flatten_lookup(lookup_result: dict[str, list[Track]]) -> list[Track]:
|
||||
def _flatten_lookup(lookup_result: dict[Uri, list[Track]]) -> list[Track]:
|
||||
tracks: list[Track] = []
|
||||
for uri_tracks in lookup_result.values():
|
||||
tracks.extend(uri_tracks)
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import tornado.web
|
||||
from mopidy.config import Config
|
||||
from mopidy.core import CoreProxy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mopidy_smartplaylists.compat import Config, CoreProxy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mopidy.models import Playlist
|
||||
|
||||
from mopidy_smartplaylists.generators import (
|
||||
build_album_mix,
|
||||
@ -35,13 +42,15 @@ class DecadeMixHandler(tornado.web.RequestHandler):
|
||||
self.write({"playlist": None, "tracks": 0})
|
||||
return
|
||||
playlist = save_smart_playlist(self.core, self.prefix, f"{decade}s Mix", tracks)
|
||||
self.write({
|
||||
self.write(
|
||||
{
|
||||
"playlist": {
|
||||
"name": playlist.name if playlist else None,
|
||||
"uri": playlist.uri if playlist else None,
|
||||
"tracks": len(tracks),
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class GenreMixHandler(tornado.web.RequestHandler):
|
||||
@ -61,13 +70,15 @@ class GenreMixHandler(tornado.web.RequestHandler):
|
||||
self.write({"playlist": None, "tracks": 0})
|
||||
return
|
||||
playlist = save_smart_playlist(self.core, self.prefix, f"{genre} Mix", tracks)
|
||||
self.write({
|
||||
self.write(
|
||||
{
|
||||
"playlist": {
|
||||
"name": playlist.name if playlist else None,
|
||||
"uri": playlist.uri if playlist else None,
|
||||
"tracks": len(tracks),
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ArtistMixHandler(tornado.web.RequestHandler):
|
||||
@ -87,13 +98,15 @@ class ArtistMixHandler(tornado.web.RequestHandler):
|
||||
self.write({"playlist": None, "tracks": 0})
|
||||
return
|
||||
playlist = save_smart_playlist(self.core, self.prefix, f"{artist} Mix", tracks)
|
||||
self.write({
|
||||
self.write(
|
||||
{
|
||||
"playlist": {
|
||||
"name": playlist.name if playlist else None,
|
||||
"uri": playlist.uri if playlist else None,
|
||||
"tracks": len(tracks),
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AlbumMixHandler(tornado.web.RequestHandler):
|
||||
@ -112,15 +125,21 @@ class AlbumMixHandler(tornado.web.RequestHandler):
|
||||
if not tracks:
|
||||
self.write({"playlist": None, "tracks": 0})
|
||||
return
|
||||
name = tracks[0].album.name if tracks[0].album and tracks[0].album.name else "Album"
|
||||
name = (
|
||||
tracks[0].album.name
|
||||
if tracks[0].album and tracks[0].album.name
|
||||
else "Album"
|
||||
)
|
||||
playlist = save_smart_playlist(self.core, self.prefix, f"{name} Mix", tracks)
|
||||
self.write({
|
||||
self.write(
|
||||
{
|
||||
"playlist": {
|
||||
"name": playlist.name if playlist else None,
|
||||
"uri": playlist.uri if playlist else None,
|
||||
"tracks": len(tracks),
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class InstantMixHandler(tornado.web.RequestHandler):
|
||||
@ -149,14 +168,18 @@ class InstantMixHandler(tornado.web.RequestHandler):
|
||||
seed_name = t.name
|
||||
break
|
||||
|
||||
playlist = save_smart_playlist(self.core, self.prefix, f"Instant Mix: {seed_name}", tracks)
|
||||
self.write({
|
||||
playlist = save_smart_playlist(
|
||||
self.core, self.prefix, f"Instant Mix: {seed_name}", tracks
|
||||
)
|
||||
self.write(
|
||||
{
|
||||
"playlist": {
|
||||
"name": playlist.name if playlist else None,
|
||||
"uri": playlist.uri if playlist else None,
|
||||
"tracks": len(tracks),
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class RefreshHandler(tornado.web.RequestHandler):
|
||||
@ -176,8 +199,10 @@ class StatusHandler(tornado.web.RequestHandler):
|
||||
|
||||
def get(self) -> None:
|
||||
result = self.core.playlists.as_list().get()
|
||||
smart = [p for p in result if p.uri and "smartplaylists" in p.uri]
|
||||
self.write({
|
||||
result_typed = cast("list[Playlist]", result)
|
||||
smart = [p for p in result_typed if p.uri and "smartplaylists" in p.uri]
|
||||
self.write(
|
||||
{
|
||||
"smart_playlists": [
|
||||
{
|
||||
"name": p.name,
|
||||
@ -187,7 +212,8 @@ class StatusHandler(tornado.web.RequestHandler):
|
||||
for p in smart
|
||||
],
|
||||
"count": len(smart),
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def app_factory(config: Config, core: CoreProxy) -> list[tuple]:
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
from unittest import mock
|
||||
|
||||
from mopidy.models import Playlist
|
||||
|
||||
from mopidy_smartplaylists.frontend import SmartPlaylistsFrontend
|
||||
|
||||
|
||||
@ -14,11 +16,16 @@ class TestSmartPlaylistsFrontend:
|
||||
"refresh_interval": "0",
|
||||
}
|
||||
core = mock.Mock()
|
||||
core.library.search.return_value.get.return_value = []
|
||||
core.playlists.lookup.return_value.get.return_value = None
|
||||
core.playlists.save.return_value.get.return_value = Playlist(
|
||||
name="test", uri="dummy:pl", tracks=[]
|
||||
)
|
||||
|
||||
frontend = SmartPlaylistsFrontend(config, core)
|
||||
frontend.on_start()
|
||||
|
||||
core.library.search.assert_called()
|
||||
assert core.library.search.call_count == 2
|
||||
|
||||
def test_on_start_without_recipes(self):
|
||||
config = mock.Mock()
|
||||
@ -39,10 +46,16 @@ class TestSmartPlaylistsFrontend:
|
||||
def test_playlists_loaded_with_interval(self):
|
||||
config = mock.Mock()
|
||||
config.get.return_value = {
|
||||
"decades": "1980",
|
||||
"playlist_prefix": "[Smart]",
|
||||
"refresh_interval": "24",
|
||||
}
|
||||
core = mock.Mock()
|
||||
core.library.search.return_value.get.return_value = []
|
||||
core.playlists.lookup.return_value.get.return_value = None
|
||||
core.playlists.save.return_value.get.return_value = Playlist(
|
||||
name="test", uri="dummy:pl", tracks=[]
|
||||
)
|
||||
|
||||
frontend = SmartPlaylistsFrontend(config, core)
|
||||
frontend.playlists_loaded()
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from mopidy.models import Artist, Playlist, SearchResult, Track
|
||||
|
||||
from mopidy_smartplaylists.generators import (
|
||||
@ -44,7 +43,10 @@ class TestSanitizeName:
|
||||
|
||||
class TestExtractTracks:
|
||||
def test_extracts_from_search_results(self):
|
||||
tracks = [Track(uri="dummy:1", name="Track 1"), Track(uri="dummy:2", name="Track 2")]
|
||||
tracks = [
|
||||
Track(uri="dummy:1", name="Track 1"),
|
||||
Track(uri="dummy:2", name="Track 2"),
|
||||
]
|
||||
results = [SearchResult(uri="dummy:search", tracks=tracks)]
|
||||
assert _extract_tracks(results) == tracks
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import tornado.testing
|
||||
import tornado.web
|
||||
from mopidy.models import Playlist, Track
|
||||
|
||||
from mopidy_smartplaylists.web import (
|
||||
@ -30,9 +31,51 @@ class TestAppFactory:
|
||||
assert "core" in kwargs
|
||||
|
||||
|
||||
class _HandlerTestBase:
|
||||
def make_app(self, handler_cls, **kwargs):
|
||||
return tornado.web.Application(
|
||||
[
|
||||
(r"/test", handler_cls, kwargs),
|
||||
]
|
||||
)
|
||||
|
||||
def call_handler(self, handler_cls, method="POST", body=None, **kwargs):
|
||||
app = self.make_app(handler_cls, **kwargs)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method=method,
|
||||
uri="/test",
|
||||
body=json.dumps(body).encode() if isinstance(body, dict) else body,
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
return handler_cls(app, request, **kwargs)
|
||||
|
||||
|
||||
class TestDecadeMixHandler:
|
||||
@pytest.fixture
|
||||
def handler(self):
|
||||
def test_missing_decade_returns_400(self):
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/decade",
|
||||
DecadeMixHandler,
|
||||
{"core": mock.Mock(), "prefix": "[Smart]"},
|
||||
),
|
||||
]
|
||||
)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method="POST",
|
||||
uri="/decade",
|
||||
body=json.dumps({}).encode(),
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
handler = DecadeMixHandler(app, request, core=mock.Mock(), prefix="[Smart]")
|
||||
handler.set_status = mock.Mock()
|
||||
handler.write = mock.Mock()
|
||||
handler.post()
|
||||
handler.set_status.assert_called_once_with(400)
|
||||
|
||||
def test_success(self):
|
||||
core = mock.Mock()
|
||||
core.library.search.return_value.get.return_value = [
|
||||
mock.Mock(tracks=[Track(uri="dummy:1", name="T")])
|
||||
@ -44,70 +87,125 @@ class TestDecadeMixHandler:
|
||||
tracks=[Track(uri="dummy:1", name="T")],
|
||||
)
|
||||
|
||||
h = DecadeMixHandler(mock.Mock(), mock.Mock(), core=core, prefix="[Smart]")
|
||||
h.request = mock.Mock()
|
||||
h.request.body = json.dumps({"decade": "1980"}).encode()
|
||||
return h
|
||||
|
||||
def test_returns_playlist_info(self, handler):
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(r"/decade", DecadeMixHandler, {"core": core, "prefix": "[Smart]"}),
|
||||
]
|
||||
)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method="POST",
|
||||
uri="/decade",
|
||||
body=json.dumps({"decade": "1980"}).encode(),
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
handler = DecadeMixHandler(app, request, core=core, prefix="[Smart]")
|
||||
handler._transforms = []
|
||||
handler.write = mock.Mock()
|
||||
handler.post()
|
||||
data = handler.write.call_args[0][0]
|
||||
assert data["playlist"]["name"] == "[Smart] 1980s Mix"
|
||||
assert data["tracks"] == 1
|
||||
|
||||
def test_missing_decade_returns_400(self):
|
||||
h = DecadeMixHandler(mock.Mock(), mock.Mock(), core=mock.Mock(), prefix="[Smart]")
|
||||
h.request = mock.Mock()
|
||||
h.request.body = json.dumps({}).encode()
|
||||
h.set_status = mock.Mock()
|
||||
h.write = mock.Mock()
|
||||
h.post()
|
||||
h.set_status.assert_called_once_with(400)
|
||||
assert data["playlist"]["tracks"] == 1
|
||||
|
||||
|
||||
class TestGenreMixHandler:
|
||||
def test_missing_genre_returns_400(self):
|
||||
h = GenreMixHandler(mock.Mock(), mock.Mock(), core=mock.Mock(), prefix="[Smart]")
|
||||
h.request = mock.Mock()
|
||||
h.request.body = json.dumps({}).encode()
|
||||
h.set_status = mock.Mock()
|
||||
h.write = mock.Mock()
|
||||
h.post()
|
||||
h.set_status.assert_called_once_with(400)
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/genre",
|
||||
GenreMixHandler,
|
||||
{"core": mock.Mock(), "prefix": "[Smart]"},
|
||||
),
|
||||
]
|
||||
)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method="POST",
|
||||
uri="/genre",
|
||||
body=json.dumps({}).encode(),
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
handler = GenreMixHandler(app, request, core=mock.Mock(), prefix="[Smart]")
|
||||
handler.set_status = mock.Mock()
|
||||
handler.write = mock.Mock()
|
||||
handler.post()
|
||||
handler.set_status.assert_called_once_with(400)
|
||||
|
||||
|
||||
class TestArtistMixHandler:
|
||||
def test_missing_artist_returns_400(self):
|
||||
h = ArtistMixHandler(mock.Mock(), mock.Mock(), core=mock.Mock(), prefix="[Smart]")
|
||||
h.request = mock.Mock()
|
||||
h.request.body = json.dumps({}).encode()
|
||||
h.set_status = mock.Mock()
|
||||
h.write = mock.Mock()
|
||||
h.post()
|
||||
h.set_status.assert_called_once_with(400)
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/artist",
|
||||
ArtistMixHandler,
|
||||
{"core": mock.Mock(), "prefix": "[Smart]"},
|
||||
),
|
||||
]
|
||||
)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method="POST",
|
||||
uri="/artist",
|
||||
body=json.dumps({}).encode(),
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
handler = ArtistMixHandler(app, request, core=mock.Mock(), prefix="[Smart]")
|
||||
handler.set_status = mock.Mock()
|
||||
handler.write = mock.Mock()
|
||||
handler.post()
|
||||
handler.set_status.assert_called_once_with(400)
|
||||
|
||||
|
||||
class TestAlbumMixHandler:
|
||||
def test_missing_uri_returns_400(self):
|
||||
h = AlbumMixHandler(mock.Mock(), mock.Mock(), core=mock.Mock(), prefix="[Smart]")
|
||||
h.request = mock.Mock()
|
||||
h.request.body = json.dumps({}).encode()
|
||||
h.set_status = mock.Mock()
|
||||
h.write = mock.Mock()
|
||||
h.post()
|
||||
h.set_status.assert_called_once_with(400)
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/album",
|
||||
AlbumMixHandler,
|
||||
{"core": mock.Mock(), "prefix": "[Smart]"},
|
||||
),
|
||||
]
|
||||
)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method="POST",
|
||||
uri="/album",
|
||||
body=json.dumps({}).encode(),
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
handler = AlbumMixHandler(app, request, core=mock.Mock(), prefix="[Smart]")
|
||||
handler.set_status = mock.Mock()
|
||||
handler.write = mock.Mock()
|
||||
handler.post()
|
||||
handler.set_status.assert_called_once_with(400)
|
||||
|
||||
|
||||
class TestInstantMixHandler:
|
||||
def test_missing_uri_returns_400(self):
|
||||
h = InstantMixHandler(mock.Mock(), mock.Mock(), core=mock.Mock(), prefix="[Smart]")
|
||||
h.request = mock.Mock()
|
||||
h.request.body = json.dumps({}).encode()
|
||||
h.set_status = mock.Mock()
|
||||
h.write = mock.Mock()
|
||||
h.post()
|
||||
h.set_status.assert_called_once_with(400)
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/instant-mix",
|
||||
InstantMixHandler,
|
||||
{"core": mock.Mock(), "prefix": "[Smart]"},
|
||||
),
|
||||
]
|
||||
)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method="POST",
|
||||
uri="/instant-mix",
|
||||
body=json.dumps({}).encode(),
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
handler = InstantMixHandler(app, request, core=mock.Mock(), prefix="[Smart]")
|
||||
handler.set_status = mock.Mock()
|
||||
handler.write = mock.Mock()
|
||||
handler.post()
|
||||
handler.set_status.assert_called_once_with(400)
|
||||
|
||||
|
||||
class TestRefreshHandler:
|
||||
@ -116,12 +214,22 @@ class TestRefreshHandler:
|
||||
config.get.return_value = {"playlist_prefix": "[Smart]"}
|
||||
core = mock.Mock()
|
||||
|
||||
h = RefreshHandler(mock.Mock(), mock.Mock(), core=core, config=config)
|
||||
h.request = mock.Mock()
|
||||
h.request.body = b"{}"
|
||||
h.write = mock.Mock()
|
||||
h.post()
|
||||
assert h.write.call_args[0][0] == {"status": "ok"}
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(r"/refresh", RefreshHandler, {"core": core, "config": config}),
|
||||
]
|
||||
)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method="POST",
|
||||
uri="/refresh",
|
||||
body=b"{}",
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
handler = RefreshHandler(app, request, core=core, config=config)
|
||||
handler.write = mock.Mock()
|
||||
handler.post()
|
||||
assert handler.write.call_args[0][0] == {"status": "ok"}
|
||||
|
||||
|
||||
class TestStatusHandler:
|
||||
@ -140,9 +248,21 @@ class TestStatusHandler:
|
||||
),
|
||||
]
|
||||
|
||||
h = StatusHandler(mock.Mock(), mock.Mock(), core=core)
|
||||
h.write = mock.Mock()
|
||||
h.get()
|
||||
data = h.write.call_args[0][0]
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(r"/status", StatusHandler, {"core": core}),
|
||||
]
|
||||
)
|
||||
request = tornado.httputil.HTTPServerRequest(
|
||||
method="GET",
|
||||
uri="/status",
|
||||
body=None,
|
||||
connection=mock.Mock(),
|
||||
headers=tornado.httputil.HTTPHeaders(),
|
||||
)
|
||||
handler = StatusHandler(app, request, core=core)
|
||||
handler.write = mock.Mock()
|
||||
handler.get()
|
||||
data = handler.write.call_args[0][0]
|
||||
assert data["count"] == 1
|
||||
assert data["smart_playlists"][0]["name"] == "[Smart] Jazz Mix"
|
||||
|
||||
Reference in New Issue
Block a user