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

import collections
import logging
import re
6
7
import time
import urllib
8
9
10
11
12
13

from mopidy import backend, models

logger = logging.getLogger(__name__)


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)
31
        return " ".join(r)
32
    if isinstance(query, list):
33
        return " ".join(query)
34
35
36
37
    else:
        return query


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):
44
45
        if self.max_age is None:
            return
46
47
48
49
        now = time.time()
        self[key] = (now, value)

    def get(self, key):
50
51
        if self.max_age is None:
            return
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):
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)
69
70
        self.vfs = {"funkwhale:directory": collections.OrderedDict()}
        self.add_to_vfs(new_folder("Favorites", "favorites"))
71
        self.add_to_vfs(new_folder("Artists", "artists"))
Georg Krause's avatar
Georg Krause committed
72
        self.add_to_vfs(new_folder("Libraries", "libraries"))
73
74
75
        # self.add_to_vfs(new_folder('Following', ['following']))
        # self.add_to_vfs(new_folder('Sets', ['sets']))
        # self.add_to_vfs(new_folder('Stream', ['stream']))
76
        self.cache = Cache(max_age=self.backend.config["funkwhale"]["cache_duration"])
77
78

    def add_to_vfs(self, _model):
79
        self.vfs["funkwhale:directory"][_model.uri] = _model
80
81

    def browse(self, uri):
82
83
84
85
86
87
88
89
90
        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]

91
        if not self.vfs.get(uri):
92
93
94
95
96
            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])
97
98
99
100
            result, cache = handler(remaining)
            if cache:
                self.cache.set(cache_key, result)
            return result
101
102

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

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

        if remaining == ["recent"]:
            payload = self.backend.client.list_tracks(
117
118
119
120
121
                {"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)
122
            ]
123
124
125
126
            return tracks, True
        return [], False

    def browse_albums(self, uri_prefix, remaining):
Georg Krause's avatar
Georg Krause committed
127
        logger.debug("Handling albums route: %s", remaining)
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
        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"),
166
                    new_folder("Own Content", "artists:scope-me"),
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
211
212
                ],
                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

213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
        if root == "scope-me":
            if end:
                # list albums
                return (
                    self.browse_albums(uri_prefix=albums_uri_prefix, remaining=end),
                    True
                )
            payload = self.backend.client.list_artists(
                {"ordering": "name", "page_size": 50, "scope": "me"}
            )
            uri_prefix = "funkwhale:directory:artists:scope-me"
            artists = [
                convert_to_artist(row, uri_prefix=uri_prefix, ref=True)
                for row in self.backend.client.load_all(payload)
            ]
            return artists, True

Georg Krause's avatar
Georg Krause committed
230
231
232
233
234
235
236
237
238
239
240
241
242
        if root == "by-library":
            logger.debug("Handling artists by lib route: %s", end)
            if len(end) == 1:
                payload = self.backend.client.list_artists(
                    {"ordering": "name", "page_size": 50, "library": end}
                )
                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

243
        return [], False
244

Georg Krause's avatar
Georg Krause committed
245
246
247
248
249
250
251
252
253
254
255
256
257
    def browse_libraries(self, remaining):
        logger.debug("Handling libraries route: %s", remaining)

        payload = self.backend.client.list_libraries(
             {"ordering": "name", "page_size": 50}
        )
        uri_prefix = "funkwhale:directory:artists:by-library"
        libraries = [
            convert_to_ref(row, uri_prefix=uri_prefix)
            for row in self.backend.client.load_all(payload)
        ]
        return libraries, True

David Sn's avatar
David Sn committed
258
259
260
261
262
263
    def get_images(self, uris):
        logger.debug("Handling get images: %s", uris)
        result = {}

        for uri in uris:
            track_id = uri.split(":")[-1]
264
265
266
267
268
269
            cache_key = "funkwhale:images:%s" % track_id
            from_cache = self.cache.get(cache_key)

            if from_cache:
                result[uri] = from_cache
                continue
David Sn's avatar
David Sn committed
270

271
            payload = self.backend.client.get_track(track_id)
David Sn's avatar
David Sn committed
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
            if not payload["album"]["cover"]:
                continue

            result[uri] = []

            for type, cover_url in payload["album"]["cover"]["urls"].items():
                if not cover_url:
                    continue

                if type == "large_square_crop":
                    image = models.Image(uri=cover_url, width=600, height=600)
                elif type == "medium_square_crop":
                    image = models.Image(uri=cover_url, width=200, height=200)
                else:
                    image = models.Image(uri=cover_url)

                result[uri].append(image)

290
291
            self.cache.set(cache_key, result[uri])

David Sn's avatar
David Sn committed
292
        return result
Georg Krause's avatar
Georg Krause committed
293

294
295
296
297
298
299
300
    def search(self, query=None, uris=None, exact=False):
        # TODO Support exact search
        if not query:
            return

        else:
            search_query = simplify_search_query(query)
301
302
303
304
305
            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"]]
306
307

            return models.SearchResult(
308
                uri="funkwhale:search", tracks=tracks, albums=albums, artists=artists
309
310
311
            )

    def lookup(self, uri):
312
313
314
315
316
317
318
319
320
321
        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:", "")
322
323
            return self.backend.remote.resolve_url(uri)

324
        client = self.backend.client
325
        config = {
326
327
328
            "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"],
329
        }
330

331
332
        type, id = parse_uri(uri)
        payload = config[type](id)
333
        return [convert_to_track(row, cache=self.cache) for row in payload]
334
335
336


def parse_uri(uri):
337
338
339
    uri = uri.replace("funkwhale:", "", 1)
    parts = uri.split(":")
    type = parts[0].rstrip("s")
340
341
342
343
    id = int(parts[1])
    return type, id


344
def cast_to_ref(f):
345
346
347
348
    def inner(payload, *args, **kwargs):
        ref = kwargs.pop("ref", False)
        cache = kwargs.pop("cache", None)
        result = f(payload, *args, **kwargs)
349
350
351
352
353
354
355
356
357
358
        if cache is not None:
            cache.set(result.uri, result)
        if ref:
            return to_ref(result)
        return result

    return inner


@cast_to_ref
359
def convert_to_artist(payload, uri_prefix="funkwhale:artists"):
360
    return models.Artist(
361
        uri=uri_prefix + ":%s" % payload["id"],
362
363
364
        name=payload["name"],
        sortname=payload["name"],
        musicbrainz_id=payload["mbid"],
365
366
367
    )


368
@cast_to_ref
369
def convert_to_album(payload, uri_prefix="funkwhale:albums"):
370
    artist = convert_to_artist(payload["artist"])
371
    return models.Album(
372
        uri=uri_prefix + ":%s" % payload["id"],
373
374
        name=payload["title"],
        musicbrainz_id=payload["mbid"],
375
        artists=[artist],
376
377
        date=payload["release_date"],
        num_tracks=len(payload.get("tracks", [])),
378
379
380
    )


381
@cast_to_ref
382
def convert_to_track(payload, uri_prefix="funkwhale:tracks"):
383
384
    artist = convert_to_artist(payload["artist"])
    album = convert_to_album(payload["album"])
David Sn's avatar
David Sn committed
385

386
387
388
389
    try:
        upload = payload["uploads"][0]
    except (KeyError, IndexError):
        upload = {}
390
    return models.Track(
391
        uri=uri_prefix + ":%s" % payload["id"],
392
393
        name=payload["title"],
        musicbrainz_id=payload["mbid"],
394
395
        artists=[artist],
        album=album,
396
        date=payload["album"]["release_date"],
Georg Krause's avatar
Georg Krause committed
397
        bitrate=int((upload.get("bitrate") or 0) / 1000),
398
        length=(upload.get("duration") or 0) * 1000,
399
        track_no=payload["position"],
400
    )
401
402


Georg Krause's avatar
Georg Krause committed
403
404
405
406
407
408
409
410
411
412
413
def convert_to_ref(payload, uri_prefix="funkwhale:libraries"):
    try:
        upload = payload["uploads"][0]
    except (KeyError, IndexError):
        upload = {}
    return models.Ref(
        uri=uri_prefix + ":%s" % payload["uuid"],
        name=payload["name"],
    )


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