mirror of
https://dev.funkwhale.audio/funkwhale/mopidy
synced 2024-11-13 20:18:25 +01:00
Can now browse favorites and artists
This commit is contained in:
parent
8696450267
commit
6a3f0949cd
9 changed files with 325 additions and 33 deletions
|
@ -15,6 +15,8 @@ Features
|
|||
--------
|
||||
|
||||
* Searching for tracks, albums and artists available in your Funkwhale instance
|
||||
* Browse all artists and albums
|
||||
* Browse your favorites
|
||||
* Simple configuration
|
||||
|
||||
Installation
|
||||
|
@ -47,15 +49,19 @@ To enable the extension, add the following to your ``mopidy.conf`` file::
|
|||
username = demo
|
||||
# Password to use when authenticating (leave empty fo anonymous access)
|
||||
password = demo
|
||||
# duration of cache entries before they are removed, in seconds
|
||||
# 0 to cache forever, empty to disable cache
|
||||
cache_duration = 600
|
||||
|
||||
Of course, replace the demo values with your actual info (but you can
|
||||
try using the demo server).
|
||||
|
||||
After that, reload your mopidy daemon, and you should be good!
|
||||
|
||||
Todo
|
||||
----
|
||||
|
||||
- Browse Funkwhale library and playlists
|
||||
- Browse use library and playlists
|
||||
|
||||
|
||||
.. _Mopidy: https://www.mopidy.com/
|
||||
|
|
|
@ -30,6 +30,7 @@ class Extension(mopidy.ext.Extension):
|
|||
schema["url"] = mopidy.config.String()
|
||||
schema["username"] = mopidy.config.String(optional=True)
|
||||
schema["password"] = mopidy.config.Secret(optional=True)
|
||||
schema["cache_duration"] = mopidy.config.Integer(optional=True)
|
||||
return schema
|
||||
|
||||
def validate_config(self, config):
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from mopidy import httpclient, exceptions
|
||||
|
||||
from . import Extension, __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionWithUrlBase(requests.Session):
|
||||
# In Python 3 you could place `url_base` after `*args`, but not in Python 2.
|
||||
|
@ -17,7 +20,10 @@ class SessionWithUrlBase(requests.Session):
|
|||
# Next line of code is here for example purposes only.
|
||||
# You really shouldn't just use string concatenation here,
|
||||
# take a look at urllib.parse.urljoin instead.
|
||||
modified_url = self.url_base + url
|
||||
if url.startswith("http://") or url.startswith("https://"):
|
||||
modified_url = url
|
||||
else:
|
||||
modified_url = self.url_base + url
|
||||
|
||||
return super(SessionWithUrlBase, self).request(method, modified_url, **kwargs)
|
||||
|
||||
|
@ -86,3 +92,31 @@ class APIClient(object):
|
|||
response = self.session.get("tracks/", params=filters)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def list_artists(self, filters):
|
||||
response = self.session.get("artists/", params=filters)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def list_albums(self, filters):
|
||||
response = self.session.get("albums/", params=filters)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def load_all(self, first_page, max=0):
|
||||
for i in first_page["results"]:
|
||||
yield i
|
||||
|
||||
next_page = first_page.get("next")
|
||||
counter = 0
|
||||
while next_page:
|
||||
logger.info("Fetching next page of result at url: %s", next_page)
|
||||
response = self.session.get(next_page)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
for i in payload["results"]:
|
||||
yield i
|
||||
counter += 1
|
||||
next_page = payload.get("next")
|
||||
if max and counter >= max:
|
||||
next_page = None
|
||||
|
|
|
@ -6,3 +6,6 @@ url = https://demo.funkwhale.audio
|
|||
username = demo
|
||||
# Password to use when authenticating (leave empty fo anonymous access)
|
||||
password = demo
|
||||
# duration of cache entries before they are removed, in seconds
|
||||
# 0 to cache forever, empty to disable cache
|
||||
cache_duration = 600
|
||||
|
|
|
@ -41,10 +41,14 @@ class Cache(collections.OrderedDict):
|
|||
super(Cache, self).__init__()
|
||||
|
||||
def set(self, key, value):
|
||||
if self.max_age is None:
|
||||
return
|
||||
now = time.time()
|
||||
self[key] = (now, value)
|
||||
|
||||
def get(self, key):
|
||||
if self.max_age is None:
|
||||
return
|
||||
value = super(Cache, self).get(key)
|
||||
if value is None:
|
||||
return
|
||||
|
@ -64,42 +68,146 @@ class FunkwhaleLibraryProvider(backend.LibraryProvider):
|
|||
super(FunkwhaleLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self.vfs = {"funkwhale:directory": collections.OrderedDict()}
|
||||
self.add_to_vfs(new_folder("Favorites", "favorites"))
|
||||
self.add_to_vfs(new_folder("Artists", "artists"))
|
||||
# self.add_to_vfs(new_folder('Following', ['following']))
|
||||
# self.add_to_vfs(new_folder('Sets', ['sets']))
|
||||
# self.add_to_vfs(new_folder('Stream', ['stream']))
|
||||
self.cache = Cache()
|
||||
self.cache = Cache(max_age=self.backend.config["funkwhale"]["cache_duration"])
|
||||
|
||||
def add_to_vfs(self, _model):
|
||||
self.vfs["funkwhale:directory"][_model.uri] = _model
|
||||
|
||||
def browse(self, uri):
|
||||
cache_key = uri
|
||||
from_cache = self.cache.get(cache_key)
|
||||
if from_cache:
|
||||
try:
|
||||
len(from_cache)
|
||||
return from_cache
|
||||
except TypeError:
|
||||
return [from_cache]
|
||||
|
||||
if not self.vfs.get(uri):
|
||||
if uri.startswith("funkwhale:directory:"):
|
||||
uri = uri.replace("funkwhale:directory:", "", 1)
|
||||
parts = uri.split(":")
|
||||
remaining = parts[1:] if len(parts) > 1 else []
|
||||
print("PARTS", parts, remaining)
|
||||
handler = getattr(self, "browse_%s" % parts[0])
|
||||
return handler(remaining)
|
||||
result, cache = handler(remaining)
|
||||
if cache:
|
||||
self.cache.set(cache_key, result)
|
||||
return result
|
||||
|
||||
# root directory
|
||||
return self.vfs.get(uri, {}).values()
|
||||
|
||||
def browse_favorites(self, remaining):
|
||||
if remaining == []:
|
||||
return [
|
||||
new_folder("Recent", "favorites:recent"),
|
||||
new_folder("By artist", "favorites:by-artist"),
|
||||
]
|
||||
return (
|
||||
[
|
||||
new_folder("Recent", "favorites:recent"),
|
||||
# new_folder("By artist", "favorites:by-artist"),
|
||||
],
|
||||
False,
|
||||
)
|
||||
|
||||
if remaining == ["recent"]:
|
||||
payload = self.backend.client.list_tracks(
|
||||
{"favorites": "true", "ordering": "-creation_date", "page_size": 100}
|
||||
)["results"]
|
||||
return [
|
||||
convert_to_track(row, ref=True, cache=self.cache) for row in payload
|
||||
{"favorites": "true", "ordering": "-creation_date", "page_size": 50}
|
||||
)
|
||||
tracks = [
|
||||
convert_to_track(row, ref=True, cache=self.cache)
|
||||
for row in self.backend.client.load_all(payload, max=10)
|
||||
]
|
||||
return []
|
||||
return tracks, True
|
||||
return [], False
|
||||
|
||||
def browse_albums(self, uri_prefix, remaining):
|
||||
if len(remaining) == 2:
|
||||
album = remaining[1]
|
||||
payload = self.backend.client.list_tracks(
|
||||
{
|
||||
"ordering": "position",
|
||||
"page_size": 50,
|
||||
"playable": "true",
|
||||
"album": album,
|
||||
}
|
||||
)
|
||||
tracks = [
|
||||
convert_to_track(row, ref=True, cache=self.cache)
|
||||
for row in self.backend.client.load_all(payload)
|
||||
]
|
||||
return tracks
|
||||
else:
|
||||
artist, album = remaining[0], None
|
||||
payload = self.backend.client.list_albums(
|
||||
{
|
||||
"ordering": "title",
|
||||
"page_size": 50,
|
||||
"playable": "true",
|
||||
"artist": artist,
|
||||
}
|
||||
)
|
||||
albums = [
|
||||
convert_to_album(row, uri_prefix=uri_prefix, ref=True)
|
||||
for row in self.backend.client.load_all(payload)
|
||||
]
|
||||
return albums
|
||||
|
||||
def browse_artists(self, remaining):
|
||||
logger.debug("Handling artist route: %s", remaining)
|
||||
if remaining == []:
|
||||
return (
|
||||
[
|
||||
new_folder("Recent", "artists:recent"),
|
||||
new_folder("By name", "artists:by-name"),
|
||||
],
|
||||
False,
|
||||
)
|
||||
|
||||
root = remaining[0]
|
||||
end = remaining[1:]
|
||||
albums_uri_prefix = "funkwhale:directory:artists:" + ":".join(
|
||||
[str(i) for i in remaining]
|
||||
)
|
||||
if root == "recent":
|
||||
if end:
|
||||
# list albums
|
||||
return (
|
||||
self.browse_albums(uri_prefix=albums_uri_prefix, remaining=end),
|
||||
True,
|
||||
)
|
||||
# list recent artists
|
||||
payload = self.backend.client.list_artists(
|
||||
{"ordering": "-creation_date", "page_size": 50, "playable": "true"}
|
||||
)
|
||||
uri_prefix = "funkwhale:directory:artists:recent"
|
||||
|
||||
artists = [
|
||||
convert_to_artist(row, uri_prefix=uri_prefix, ref=True)
|
||||
for row in self.backend.client.load_all(payload, max=1)
|
||||
]
|
||||
return artists, True
|
||||
|
||||
if root == "by-name":
|
||||
if end:
|
||||
# list albums
|
||||
return (
|
||||
self.browse_albums(uri_prefix=albums_uri_prefix, remaining=end),
|
||||
True,
|
||||
)
|
||||
# list recent artists
|
||||
payload = self.backend.client.list_artists(
|
||||
{"ordering": "name", "page_size": 50, "playable": "true"}
|
||||
)
|
||||
uri_prefix = "funkwhale:directory:artists:by-name"
|
||||
artists = [
|
||||
convert_to_artist(row, uri_prefix=uri_prefix, ref=True)
|
||||
for row in self.backend.client.load_all(payload)
|
||||
]
|
||||
return artists, True
|
||||
|
||||
return [], False
|
||||
|
||||
def search(self, query=None, uris=None, exact=False):
|
||||
# TODO Support exact search
|
||||
|
@ -119,7 +227,6 @@ class FunkwhaleLibraryProvider(backend.LibraryProvider):
|
|||
)
|
||||
|
||||
def lookup(self, uri):
|
||||
print("CACHE", self.cache, uri)
|
||||
from_cache = self.cache.get(uri)
|
||||
if from_cache:
|
||||
try:
|
||||
|
@ -153,8 +260,10 @@ def parse_uri(uri):
|
|||
|
||||
|
||||
def cast_to_ref(f):
|
||||
def inner(payload, ref=False, cache=None):
|
||||
result = f(payload)
|
||||
def inner(payload, *args, **kwargs):
|
||||
ref = kwargs.pop("ref", False)
|
||||
cache = kwargs.pop("cache", None)
|
||||
result = f(payload, *args, **kwargs)
|
||||
if cache is not None:
|
||||
cache.set(result.uri, result)
|
||||
if ref:
|
||||
|
@ -165,9 +274,9 @@ def cast_to_ref(f):
|
|||
|
||||
|
||||
@cast_to_ref
|
||||
def convert_to_artist(payload):
|
||||
def convert_to_artist(payload, uri_prefix="funkwhale:artists"):
|
||||
return models.Artist(
|
||||
uri="funkwhale:artists:%s" % (payload["id"],),
|
||||
uri=uri_prefix + ":%s" % payload["id"],
|
||||
name=payload["name"],
|
||||
sortname=payload["name"],
|
||||
musicbrainz_id=payload["mbid"],
|
||||
|
@ -175,12 +284,12 @@ def convert_to_artist(payload):
|
|||
|
||||
|
||||
@cast_to_ref
|
||||
def convert_to_album(payload):
|
||||
def convert_to_album(payload, uri_prefix="funkwhale:albums"):
|
||||
artist = convert_to_artist(payload["artist"])
|
||||
image = payload["cover"]["original"] if payload["cover"] else None
|
||||
|
||||
return models.Album(
|
||||
uri="funkwhale:albums:%s" % (payload["id"],),
|
||||
uri=uri_prefix + ":%s" % payload["id"],
|
||||
name=payload["title"],
|
||||
musicbrainz_id=payload["mbid"],
|
||||
images=[image] if image else [],
|
||||
|
@ -191,11 +300,11 @@ def convert_to_album(payload):
|
|||
|
||||
|
||||
@cast_to_ref
|
||||
def convert_to_track(payload):
|
||||
def convert_to_track(payload, uri_prefix="funkwhale:tracks"):
|
||||
artist = convert_to_artist(payload["artist"])
|
||||
album = convert_to_album(payload["album"])
|
||||
return models.Track(
|
||||
uri="funkwhale:tracks:%s" % (payload["id"],),
|
||||
uri=uri_prefix + ":%s" % payload["id"],
|
||||
name=payload["title"],
|
||||
musicbrainz_id=payload["mbid"],
|
||||
artists=[artist],
|
||||
|
|
|
@ -10,7 +10,12 @@ FUNKWHALE_URL = "https://test.funkwhale"
|
|||
@pytest.fixture()
|
||||
def config():
|
||||
return {
|
||||
"funkwhale": {"url": FUNKWHALE_URL, "username": "user", "password": "passw0rd"},
|
||||
"funkwhale": {
|
||||
"url": FUNKWHALE_URL,
|
||||
"username": "user",
|
||||
"password": "passw0rd",
|
||||
"cache_duration": 600,
|
||||
},
|
||||
"proxy": {},
|
||||
}
|
||||
|
||||
|
|
|
@ -24,3 +24,33 @@ def test_client_list_tracks(client, requests_mock):
|
|||
|
||||
result = client.list_tracks({"artist": 12})
|
||||
assert result == {"hello": "world"}
|
||||
|
||||
|
||||
def test_client_list_artists(client, requests_mock):
|
||||
requests_mock.get(
|
||||
client.session.url_base + "artists/?playable=true", json={"hello": "world"}
|
||||
)
|
||||
|
||||
result = client.list_artists({"playable": "true"})
|
||||
assert result == {"hello": "world"}
|
||||
|
||||
|
||||
def test_client_list_albums(client, requests_mock):
|
||||
requests_mock.get(
|
||||
client.session.url_base + "albums/?playable=true", json={"hello": "world"}
|
||||
)
|
||||
|
||||
result = client.list_albums({"playable": "true"})
|
||||
assert result == {"hello": "world"}
|
||||
|
||||
|
||||
def test_load_all(client, requests_mock):
|
||||
page1 = {"results": [1, 2, 3], "next": "https://first.page"}
|
||||
page2 = {"results": [4, 5, 6], "next": "https://second.page"}
|
||||
page3 = {"results": [7, 8, 9], "next": None}
|
||||
requests_mock.get(page1["next"], json=page2)
|
||||
requests_mock.get(page2["next"], json=page3)
|
||||
assert (
|
||||
list(client.load_all(page1))
|
||||
== page1["results"] + page2["results"] + page3["results"]
|
||||
)
|
||||
|
|
|
@ -22,3 +22,4 @@ def test_get_config_schema():
|
|||
assert "url" in schema
|
||||
assert "username" in schema
|
||||
assert "password" in schema
|
||||
assert "cache_duration" in schema
|
||||
|
|
|
@ -113,19 +113,26 @@ def test_parse_uri(type):
|
|||
],
|
||||
)
|
||||
def test_browse_routing(library, path, expected_handler, mocker, remaining):
|
||||
handler = mocker.patch.object(library, expected_handler, return_value="test")
|
||||
handler = mocker.patch.object(
|
||||
library, expected_handler, return_value=("test", False)
|
||||
)
|
||||
|
||||
assert library.browse(path) == "test"
|
||||
assert handler.called_once_with(remaining)
|
||||
|
||||
|
||||
def test_browse_favorites_root(library):
|
||||
expected = [
|
||||
models.Ref.directory(uri="funkwhale:directory:favorites:recent", name="Recent"),
|
||||
models.Ref.directory(
|
||||
uri="funkwhale:directory:favorites:by-artist", name="By artist"
|
||||
),
|
||||
]
|
||||
expected = (
|
||||
[
|
||||
models.Ref.directory(
|
||||
uri="funkwhale:directory:favorites:recent", name="Recent"
|
||||
),
|
||||
# models.Ref.directory(
|
||||
# uri="funkwhale:directory:favorites:by-artist", name="By artist"
|
||||
# ),
|
||||
],
|
||||
False,
|
||||
)
|
||||
assert library.browse_favorites([]) == expected
|
||||
|
||||
|
||||
|
@ -133,16 +140,96 @@ def test_browse_favorites_recent(library, client, requests_mock):
|
|||
track = factories.TrackJSONFactory()
|
||||
url = (
|
||||
client.session.url_base
|
||||
+ "tracks/?favorites=true&page_size=100&&ordering=-creation_date"
|
||||
+ "tracks/?favorites=true&page_size=50&&ordering=-creation_date"
|
||||
)
|
||||
requests_mock.get(url, json={"results": [track]})
|
||||
|
||||
expected = [mopidy_funkwhale.library.convert_to_track(track, ref=True)]
|
||||
expected = [mopidy_funkwhale.library.convert_to_track(track, ref=True)], True
|
||||
result = library.browse_favorites(["recent"])
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_browse_artists_root(library):
|
||||
expected = (
|
||||
[
|
||||
models.Ref.directory(
|
||||
uri="funkwhale:directory:artists:recent", name="Recent"
|
||||
),
|
||||
models.Ref.directory(
|
||||
uri="funkwhale:directory:artists:by-name", name="By name"
|
||||
),
|
||||
],
|
||||
False,
|
||||
)
|
||||
assert library.browse_artists([]) == expected
|
||||
|
||||
|
||||
def test_browse_artists_recent(client, library, requests_mock):
|
||||
artist1 = factories.ArtistJSONFactory()
|
||||
artist2 = factories.ArtistJSONFactory()
|
||||
url = (
|
||||
client.session.url_base
|
||||
+ "artists/?page_size=50&ordering=-creation_date&playable=true"
|
||||
)
|
||||
requests_mock.get(url, json={"results": [artist1, artist2]})
|
||||
uri_prefix = "funkwhale:directory:artists:recent"
|
||||
|
||||
expected = (
|
||||
[
|
||||
mopidy_funkwhale.library.convert_to_artist(
|
||||
artist1, uri_prefix=uri_prefix, ref=True
|
||||
),
|
||||
mopidy_funkwhale.library.convert_to_artist(
|
||||
artist2, uri_prefix=uri_prefix, ref=True
|
||||
),
|
||||
],
|
||||
True,
|
||||
)
|
||||
assert library.browse_artists(["recent"]) == expected
|
||||
|
||||
|
||||
def test_browse_artists_albums(client, library, requests_mock):
|
||||
album1 = factories.AlbumJSONFactory()
|
||||
album2 = factories.AlbumJSONFactory(artist=album1["artist"])
|
||||
url = (
|
||||
client.session.url_base
|
||||
+ "albums/?page_size=50&ordering=title&playable=true&artist%s"
|
||||
% album1["artist"]["id"]
|
||||
)
|
||||
requests_mock.get(url, json={"results": [album1, album2]})
|
||||
uri_prefix = "funkwhale:directory:artists:by-name:%s" % album1["artist"]["id"]
|
||||
expected = (
|
||||
[
|
||||
mopidy_funkwhale.library.convert_to_album(
|
||||
album1, uri_prefix=uri_prefix, ref=True
|
||||
),
|
||||
mopidy_funkwhale.library.convert_to_album(
|
||||
album2, uri_prefix=uri_prefix, ref=True
|
||||
),
|
||||
],
|
||||
True,
|
||||
)
|
||||
assert library.browse_artists(["by-name", album1["artist"]["id"]]) == expected
|
||||
|
||||
|
||||
def test_browse_artists_album_single(client, library, requests_mock):
|
||||
track = factories.TrackJSONFactory()
|
||||
url = (
|
||||
client.session.url_base
|
||||
+ "tracks/?page_size=50&ordering=position&playable=true&album="
|
||||
+ str(track["album"]["id"])
|
||||
)
|
||||
requests_mock.get(url, json={"results": [track]})
|
||||
expected = ([mopidy_funkwhale.library.convert_to_track(track, ref=True)], True)
|
||||
assert (
|
||||
library.browse_artists(
|
||||
["by-name", track["album"]["artist"]["id"], track["album"]["id"]]
|
||||
)
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
def test_cache_set():
|
||||
cache = mopidy_funkwhale.library.Cache()
|
||||
cache.set("hello:world", "value")
|
||||
|
@ -162,3 +249,19 @@ def test_cache_key_too_old():
|
|||
cache["hello:world"] = (t, "value")
|
||||
assert cache.get("hello:world") is None
|
||||
assert "hello:world" not in cache
|
||||
|
||||
|
||||
def test_lookup_from_cache(library):
|
||||
track = object()
|
||||
library.cache.set("funkwhale:artists:42", track)
|
||||
|
||||
result = library.lookup("funkwhale:artists:42")
|
||||
assert result == [track]
|
||||
|
||||
|
||||
def test_lookup_from_cache_iterable(library):
|
||||
track = [object()]
|
||||
library.cache.set("funkwhale:artists:42", track)
|
||||
|
||||
result = library.lookup("funkwhale:artists:42")
|
||||
assert result == track
|
||||
|
|
Loading…
Reference in a new issue