Can now browse favorites and artists

main
Eliot Berriot 2018-10-05 00:06:09 +02:00
parent 8696450267
commit 6a3f0949cd
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
9 changed files with 325 additions and 33 deletions

View File

@ -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/

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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],

View File

@ -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": {},
}

View File

@ -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"]
)

View File

@ -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

View File

@ -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