library.py 10.1 KB
Newer Older
1
2
3
4
5
from __future__ import unicode_literals

import collections
import logging
import re
Agate's avatar
Agate committed
6
7
import time
import urllib
8
9
10
11
12
13

from mopidy import backend, models

logger = logging.getLogger(__name__)


Agate's avatar
Agate committed
14
15
16
17
18
19
20
def generate_uri(path):
    return "funkwhale:directory:%s" % path


def new_folder(name, path):
    return models.Ref.directory(uri=generate_uri(path), name=name)

21
22
23
24
25
26
27
28
29
30

def simplify_search_query(query):

    if isinstance(query, dict):
        r = []
        for v in query.values():
            if isinstance(v, list):
                r.extend(v)
            else:
                r.append(v)
Agate's avatar
Agate committed
31
        return " ".join(r)
32
    if isinstance(query, list):
Agate's avatar
Agate committed
33
        return " ".join(query)
34
35
36
37
    else:
        return query


Agate's avatar
Agate committed
38
39
40
41
42
43
class Cache(collections.OrderedDict):
    def __init__(self, max_age=0):
        self.max_age = max_age
        super(Cache, self).__init__()

    def set(self, key, value):
Agate's avatar
Agate committed
44
45
        if self.max_age is None:
            return
Agate's avatar
Agate committed
46
47
48
49
        now = time.time()
        self[key] = (now, value)

    def get(self, key):
Agate's avatar
Agate committed
50
51
        if self.max_age is None:
            return
Agate's avatar
Agate committed
52
53
54
55
56
57
58
59
60
61
62
63
        value = super(Cache, self).get(key)
        if value is None:
            return
        now = time.time()
        t, v = value
        if self.max_age and t + self.max_age < now:
            # entry is too old, we delete it
            del self[key]
            return None
        return v


64
class FunkwhaleLibraryProvider(backend.LibraryProvider):
Agate's avatar
Agate committed
65
    root_directory = models.Ref.directory(uri="funkwhale:directory", name="Funkwhale")
66
67
68

    def __init__(self, *args, **kwargs):
        super(FunkwhaleLibraryProvider, self).__init__(*args, **kwargs)
Agate's avatar
Agate committed
69
70
        self.vfs = {"funkwhale:directory": collections.OrderedDict()}
        self.add_to_vfs(new_folder("Favorites", "favorites"))
Agate's avatar
Agate committed
71
        self.add_to_vfs(new_folder("Artists", "artists"))
72
73
74
        # self.add_to_vfs(new_folder('Following', ['following']))
        # self.add_to_vfs(new_folder('Sets', ['sets']))
        # self.add_to_vfs(new_folder('Stream', ['stream']))
Agate's avatar
Agate committed
75
        self.cache = Cache(max_age=self.backend.config["funkwhale"]["cache_duration"])
76
77

    def add_to_vfs(self, _model):
Agate's avatar
Agate committed
78
        self.vfs["funkwhale:directory"][_model.uri] = _model
79
80

    def browse(self, uri):
Agate's avatar
Agate committed
81
82
83
84
85
86
87
88
89
        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]

90
        if not self.vfs.get(uri):
Agate's avatar
Agate committed
91
92
93
94
95
            if uri.startswith("funkwhale:directory:"):
                uri = uri.replace("funkwhale:directory:", "", 1)
            parts = uri.split(":")
            remaining = parts[1:] if len(parts) > 1 else []
            handler = getattr(self, "browse_%s" % parts[0])
Agate's avatar
Agate committed
96
97
98
99
            result, cache = handler(remaining)
            if cache:
                self.cache.set(cache_key, result)
            return result
100
101

        # root directory
Georg Krause's avatar
Georg Krause committed
102
        return list(self.vfs.get(uri, {}).values())
103

Agate's avatar
Agate committed
104
105
    def browse_favorites(self, remaining):
        if remaining == []:
Agate's avatar
Agate committed
106
107
108
109
110
111
112
            return (
                [
                    new_folder("Recent", "favorites:recent"),
                    # new_folder("By artist", "favorites:by-artist"),
                ],
                False,
            )
Agate's avatar
Agate committed
113
114
115

        if remaining == ["recent"]:
            payload = self.backend.client.list_tracks(
Agate's avatar
Agate committed
116
117
118
119
120
                {"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)
Agate's avatar
Agate committed
121
            ]
Agate's avatar
Agate committed
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
            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
Agate's avatar
Agate committed
211

212
213
214
215
216
217
218
    def search(self, query=None, uris=None, exact=False):
        # TODO Support exact search
        if not query:
            return

        else:
            search_query = simplify_search_query(query)
Agate's avatar
Agate committed
219
220
221
222
223
            logger.info("Searching Funkwhale for: %s", search_query)
            raw_results = self.backend.client.search(search_query)
            artists = [convert_to_artist(row) for row in raw_results["artists"]]
            albums = [convert_to_album(row) for row in raw_results["albums"]]
            tracks = [convert_to_track(row) for row in raw_results["tracks"]]
224
225

            return models.SearchResult(
Agate's avatar
Agate committed
226
                uri="funkwhale:search", tracks=tracks, albums=albums, artists=artists
227
228
229
            )

    def lookup(self, uri):
Agate's avatar
Agate committed
230
231
232
233
234
235
236
237
238
239
        from_cache = self.cache.get(uri)
        if from_cache:
            try:
                len(from_cache)
                return from_cache
            except TypeError:
                return [from_cache]

        if "fw:" in uri:
            uri = uri.replace("fw:", "")
240
241
            return self.backend.remote.resolve_url(uri)

Agate's avatar
Agate committed
242
        client = self.backend.client
243
        config = {
Agate's avatar
Agate committed
244
245
246
            "track": lambda id: [client.get_track(id)],
            "album": lambda id: client.list_tracks({"album": id})["results"],
            "artist": lambda id: client.list_tracks({"artist": id})["results"],
247
        }
Agate's avatar
Agate committed
248

249
250
        type, id = parse_uri(uri)
        payload = config[type](id)
Agate's avatar
Agate committed
251
        return [convert_to_track(row, cache=self.cache) for row in payload]
252
253
254


def parse_uri(uri):
Agate's avatar
Agate committed
255
256
257
    uri = uri.replace("funkwhale:", "", 1)
    parts = uri.split(":")
    type = parts[0].rstrip("s")
258
259
260
261
    id = int(parts[1])
    return type, id


Agate's avatar
Agate committed
262
def cast_to_ref(f):
Agate's avatar
Agate committed
263
264
265
266
    def inner(payload, *args, **kwargs):
        ref = kwargs.pop("ref", False)
        cache = kwargs.pop("cache", None)
        result = f(payload, *args, **kwargs)
Agate's avatar
Agate committed
267
268
269
270
271
272
273
274
275
276
        if cache is not None:
            cache.set(result.uri, result)
        if ref:
            return to_ref(result)
        return result

    return inner


@cast_to_ref
Agate's avatar
Agate committed
277
def convert_to_artist(payload, uri_prefix="funkwhale:artists"):
278
    return models.Artist(
Agate's avatar
Agate committed
279
        uri=uri_prefix + ":%s" % payload["id"],
Agate's avatar
Agate committed
280
281
282
        name=payload["name"],
        sortname=payload["name"],
        musicbrainz_id=payload["mbid"],
283
284
285
    )


Agate's avatar
Agate committed
286
@cast_to_ref
Agate's avatar
Agate committed
287
def convert_to_album(payload, uri_prefix="funkwhale:albums"):
Agate's avatar
Agate committed
288
289
    artist = convert_to_artist(payload["artist"])
    image = payload["cover"]["original"] if payload["cover"] else None
290
291

    return models.Album(
Agate's avatar
Agate committed
292
        uri=uri_prefix + ":%s" % payload["id"],
Agate's avatar
Agate committed
293
294
        name=payload["title"],
        musicbrainz_id=payload["mbid"],
295
        artists=[artist],
Agate's avatar
Agate committed
296
297
        date=payload["release_date"],
        num_tracks=len(payload.get("tracks", [])),
298
299
300
    )


Agate's avatar
Agate committed
301
@cast_to_ref
Agate's avatar
Agate committed
302
def convert_to_track(payload, uri_prefix="funkwhale:tracks"):
Agate's avatar
Agate committed
303
304
    artist = convert_to_artist(payload["artist"])
    album = convert_to_album(payload["album"])
305
306
307
308
    try:
        upload = payload["uploads"][0]
    except (KeyError, IndexError):
        upload = {}
309
    return models.Track(
Agate's avatar
Agate committed
310
        uri=uri_prefix + ":%s" % payload["id"],
Agate's avatar
Agate committed
311
312
        name=payload["title"],
        musicbrainz_id=payload["mbid"],
313
314
        artists=[artist],
        album=album,
Agate's avatar
Agate committed
315
        date=payload["album"]["release_date"],
Georg Krause's avatar
Georg Krause committed
316
        bitrate=int((upload.get("bitrate") or 0) / 1000),
317
        length=(upload.get("duration") or 0) * 1000,
Agate's avatar
Agate committed
318
        track_no=payload["position"],
319
    )
Agate's avatar
Agate committed
320
321
322
323
324


def to_ref(obj):
    getter = getattr(models.Ref, obj.__class__.__name__.lower())
    return getter(uri=obj.uri, name=obj.name)