Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
funkwhale
mopidy
Commits
6a3f0949
Verified
Commit
6a3f0949
authored
Oct 05, 2018
by
Eliot Berriot
Browse files
Can now browse favorites and artists
parent
86964502
Pipeline
#2192
passed with stage
in 1 minute
Changes
9
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
README.rst
View file @
6a3f0949
...
...
@@ -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
Funkwhal
e library and playlists
- Browse
us
e library and playlists
.. _Mopidy: https://www.mopidy.com/
...
...
mopidy_funkwhale/__init__.py
View file @
6a3f0949
...
...
@@ -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
):
...
...
mopidy_funkwhale/client.py
View file @
6a3f0949
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
mopidy_funkwhale/ext.conf
View file @
6a3f0949
...
...
@@ -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
mopidy_funkwhale/library.py
View file @
6a3f0949
...
...
@@ -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
],
...
...
tests/conftest.py
View file @
6a3f0949
...
...
@@ -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"
:
{},
}
...
...
tests/test_client.py
View file @
6a3f0949
...
...
@@ -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"
]
)
tests/test_extension.py
View file @
6a3f0949
...
...
@@ -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
tests/test_library.py
View file @
6a3f0949
...
...
@@ -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=
10
0&&ordering=-creation_date"
+
"tracks/?favorites=true&page_size=
5
0&&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
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment