views.py 23.1 KB
Newer Older
1
import datetime
Eliot Berriot's avatar
Eliot Berriot committed
2
import functools
3

4
from django.conf import settings
5
from django.utils import timezone
6
7
from rest_framework import exceptions
from rest_framework import permissions as rest_permissions
Eliot Berriot's avatar
Eliot Berriot committed
8
from rest_framework import renderers, response, viewsets
Eliot Berriot's avatar
Eliot Berriot committed
9
from rest_framework.decorators import action
10
11
from rest_framework.serializers import ValidationError

12
import funkwhale_api
13
from funkwhale_api.activity import record
14
from funkwhale_api.common import fields, preferences, utils as common_utils
15
from funkwhale_api.favorites.models import TrackFavorite
16
from funkwhale_api.moderation import filters as moderation_filters
17
from funkwhale_api.music import models as music_models
18
from funkwhale_api.music import utils
19
from funkwhale_api.music import views as music_views
20
from funkwhale_api.playlists import models as playlists_models
21
from funkwhale_api.users import models as users_models
22

Eliot Berriot's avatar
Eliot Berriot committed
23
from . import authentication, filters, negotiation, serializers
24
25


26
27
28
def find_object(
    queryset, model_field="pk", field="id", cast=int, filter_playable=False
):
29
    def decorator(func):
Eliot Berriot's avatar
Eliot Berriot committed
30
        @functools.wraps(func)
31
32
33
34
35
        def inner(self, request, *args, **kwargs):
            data = request.GET or request.POST
            try:
                raw_value = data[field]
            except KeyError:
Eliot Berriot's avatar
Eliot Berriot committed
36
37
38
39
40
41
42
43
                return response.Response(
                    {
                        "error": {
                            "code": 10,
                            "message": "required parameter '{}' not present".format(
                                field
                            ),
                        }
44
                    }
Eliot Berriot's avatar
Eliot Berriot committed
45
                )
46
47
            try:
                value = cast(raw_value)
48
            except (ValueError, TypeError, ValidationError):
Eliot Berriot's avatar
Eliot Berriot committed
49
50
51
52
53
54
                return response.Response(
                    {
                        "error": {
                            "code": 0,
                            "message": 'For input string "{}"'.format(raw_value),
                        }
55
                    }
Eliot Berriot's avatar
Eliot Berriot committed
56
                )
57
            qs = queryset
Eliot Berriot's avatar
Eliot Berriot committed
58
            if hasattr(qs, "__call__"):
59
                qs = qs(request)
60
61
62

            if filter_playable:
                actor = utils.get_actor_from_request(request)
63
                qs = qs.playable_by(actor)
64

65
            try:
66
67
                obj = qs.get(**{model_field: value})
            except qs.model.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
68
69
70
71
                return response.Response(
                    {
                        "error": {
                            "code": 70,
72
                            "message": "{} not found".format(qs.model.__name__),
Eliot Berriot's avatar
Eliot Berriot committed
73
                        }
74
                    }
Eliot Berriot's avatar
Eliot Berriot committed
75
76
                )
            kwargs["obj"] = obj
77
            return func(self, request, *args, **kwargs)
Eliot Berriot's avatar
Eliot Berriot committed
78

79
        return inner
Eliot Berriot's avatar
Eliot Berriot committed
80

81
82
83
    return decorator


84
85
86
87
88
89
90
91
def get_playlist_qs(request):
    qs = playlists_models.Playlist.objects.filter(
        fields.privacy_level_query(request.user)
    )
    qs = qs.with_tracks_count().exclude(_tracks_count=0).select_related("user")
    return qs.order_by("-creation_date")


92
93
94
class SubsonicViewSet(viewsets.GenericViewSet):
    content_negotiation_class = negotiation.SubsonicContentNegociation
    authentication_classes = [authentication.SubsonicAuthentication]
95
    permission_classes = [rest_permissions.IsAuthenticated]
96

97
    def dispatch(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
98
        if not preferences.get("subsonic__enabled"):
99
100
            r = response.Response({}, status=405)
            r.accepted_renderer = renderers.JSONRenderer()
Eliot Berriot's avatar
Eliot Berriot committed
101
            r.accepted_media_type = "application/json"
102
103
104
105
            r.renderer_context = {}
            return r
        return super().dispatch(request, *args, **kwargs)

106
107
108
    def handle_exception(self, exc):
        # subsonic API sends 200 status code with custom error
        # codes in the payload
109
110
111
112
        mapping = {
            exceptions.AuthenticationFailed: (40, "Wrong username or password."),
            exceptions.NotAuthenticated: (10, "Required parameter is missing."),
        }
Eliot Berriot's avatar
Eliot Berriot committed
113
        payload = {"status": "failed"}
114
        if exc.__class__ in mapping:
115
116
            code, message = mapping[exc.__class__]
        else:
117
            return super().handle_exception(exc)
Eliot Berriot's avatar
Eliot Berriot committed
118
        payload["error"] = {"code": code, "message": message}
119
120
121

        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
122
    @action(detail=False, methods=["get", "post"], permission_classes=[])
123
    def ping(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
124
        data = {"status": "ok", "version": "1.16.0"}
125
126
        return response.Response(data, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
127
128
    @action(
        detail=False,
Eliot Berriot's avatar
Eliot Berriot committed
129
130
        methods=["get", "post"],
        url_name="get_license",
131
        permission_classes=[],
Eliot Berriot's avatar
Eliot Berriot committed
132
133
        url_path="getLicense",
    )
134
135
136
    def get_license(self, request, *args, **kwargs):
        now = timezone.now()
        data = {
Eliot Berriot's avatar
Eliot Berriot committed
137
138
            "status": "ok",
            "version": "1.16.0",
139
            "type": "funkwhale",
140
            "funkwhaleVersion": funkwhale_api.__version__,
Eliot Berriot's avatar
Eliot Berriot committed
141
142
143
144
145
            "license": {
                "valid": "true",
                "email": "valid@valid.license",
                "licenseExpires": now + datetime.timedelta(days=365),
            },
146
147
148
        }
        return response.Response(data, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
149
150
151
152
153
154
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_artists",
        url_path="getArtists",
    )
155
    def get_artists(self, request, *args, **kwargs):
156
157
158
159
160
161
162
163
        artists = (
            music_models.Artist.objects.all()
            .exclude(
                moderation_filters.get_filtered_content_query(
                    moderation_filters.USER_FILTER_CONFIG["ARTIST"], request.user
                )
            )
            .playable_by(utils.get_actor_from_request(request))
164
        )
165
        data = serializers.GetArtistsSerializer(artists).data
Eliot Berriot's avatar
Eliot Berriot committed
166
        payload = {"artists": data}
167
168
169

        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
170
171
172
173
174
175
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_indexes",
        url_path="getIndexes",
    )
176
    def get_indexes(self, request, *args, **kwargs):
177
178
179
180
181
182
183
184
        artists = (
            music_models.Artist.objects.all()
            .exclude(
                moderation_filters.get_filtered_content_query(
                    moderation_filters.USER_FILTER_CONFIG["ARTIST"], request.user
                )
            )
            .playable_by(utils.get_actor_from_request(request))
185
        )
186
        data = serializers.GetArtistsSerializer(artists).data
Eliot Berriot's avatar
Eliot Berriot committed
187
        payload = {"indexes": data}
188
189
190

        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
191
192
193
194
195
196
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_artist",
        url_path="getArtist",
    )
197
    @find_object(music_models.Artist.objects.all(), filter_playable=True)
198
    def get_artist(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
199
        artist = kwargs.pop("obj")
200
        data = serializers.GetArtistSerializer(artist).data
Eliot Berriot's avatar
Eliot Berriot committed
201
        payload = {"artist": data}
202
203
204

        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
205
206
207
    @action(
        detail=False, methods=["get", "post"], url_name="get_song", url_path="getSong"
    )
208
    @find_object(music_models.Track.objects.all(), filter_playable=True)
209
210
211
212
213
214
215
    def get_song(self, request, *args, **kwargs):
        track = kwargs.pop("obj")
        data = serializers.GetSongSerializer(track).data
        payload = {"song": data}

        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
216
217
218
219
220
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_artist_info2",
        url_path="getArtistInfo2",
Eliot Berriot's avatar
Eliot Berriot committed
221
    )
222
    @find_object(music_models.Artist.objects.all(), filter_playable=True)
223
    def get_artist_info2(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
224
        payload = {"artist-info2": {}}
225
226
227

        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
228
229
230
    @action(
        detail=False, methods=["get", "post"], url_name="get_album", url_path="getAlbum"
    )
231
232
233
    @find_object(
        music_models.Album.objects.select_related("artist"), filter_playable=True
    )
234
    def get_album(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
235
        album = kwargs.pop("obj")
236
        data = serializers.GetAlbumSerializer(album).data
Eliot Berriot's avatar
Eliot Berriot committed
237
        payload = {"album": data}
238
239
        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
240
    @action(detail=False, methods=["get", "post"], url_name="stream", url_path="stream")
241
    @find_object(music_models.Track.objects.all(), filter_playable=True)
242
    def stream(self, request, *args, **kwargs):
243
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
244
        track = kwargs.pop("obj")
Eliot Berriot's avatar
Eliot Berriot committed
245
246
247
        queryset = track.uploads.select_related("track__album__artist", "track__artist")
        upload = queryset.first()
        if not upload:
248
            return response.Response(status=404)
249

250
251
252
253
254
255
256
257
        max_bitrate = data.get("maxBitRate")
        try:
            max_bitrate = min(max(int(max_bitrate), 0), 320) or None
        except (TypeError, ValueError):
            max_bitrate = None

        if max_bitrate:
            max_bitrate = max_bitrate * 1000
258
259
260
261
262
263
264
265
266
267

        format = data.get("format", "raw") or None
        if max_bitrate and not format:
            # specific bitrate requested, but no format specified
            # so we use a default one, cf #867. This helps with clients
            # that don't send the format parameter, such as DSub.
            format = settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT
        elif format == "raw":
            format = None

268
        return music_views.handle_serve(
269
270
271
272
273
274
275
            upload=upload,
            user=request.user,
            format=format,
            max_bitrate=max_bitrate,
            # Subsonic clients don't expect 302 redirection unfortunately,
            # So we have to proxy media files
            proxy_media=True,
276
        )
277

Eliot Berriot's avatar
Eliot Berriot committed
278
    @action(detail=False, methods=["get", "post"], url_name="star", url_path="star")
Eliot Berriot's avatar
Eliot Berriot committed
279
    @find_object(music_models.Track.objects.all())
280
    def star(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
281
        track = kwargs.pop("obj")
282
        TrackFavorite.add(user=request.user, track=track)
Eliot Berriot's avatar
Eliot Berriot committed
283
        return response.Response({"status": "ok"})
284

Eliot Berriot's avatar
Eliot Berriot committed
285
    @action(detail=False, methods=["get", "post"], url_name="unstar", url_path="unstar")
Eliot Berriot's avatar
Eliot Berriot committed
286
    @find_object(music_models.Track.objects.all())
287
    def unstar(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
288
        track = kwargs.pop("obj")
289
        request.user.track_favorites.filter(track=track).delete()
Eliot Berriot's avatar
Eliot Berriot committed
290
        return response.Response({"status": "ok"})
291

Eliot Berriot's avatar
Eliot Berriot committed
292
293
294
295
296
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_starred2",
        url_path="getStarred2",
Eliot Berriot's avatar
Eliot Berriot committed
297
    )
298
299
    def get_starred2(self, request, *args, **kwargs):
        favorites = request.user.track_favorites.all()
Eliot Berriot's avatar
Eliot Berriot committed
300
        data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}}
301
302
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
303
304
305
306
307
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_random_songs",
        url_path="getRandomSongs",
308
309
310
311
    )
    def get_random_songs(self, request, *args, **kwargs):
        data = request.GET or request.POST
        actor = utils.get_actor_from_request(request)
312
313
314
315
316
        queryset = music_models.Track.objects.all().exclude(
            moderation_filters.get_filtered_content_query(
                moderation_filters.USER_FILTER_CONFIG["TRACK"], request.user
            )
        )
317
318
319
320
321
322
        queryset = queryset.playable_by(actor)
        try:
            size = int(data["size"])
        except (TypeError, KeyError, ValueError):
            size = 50

Eliot Berriot's avatar
Eliot Berriot committed
323
324
325
        queryset = (
            queryset.playable_by(actor).prefetch_related("uploads").order_by("?")[:size]
        )
326
327
328
329
330
331
332
        data = {
            "randomSongs": {
                "song": serializers.GetSongSerializer(queryset, many=True).data
            }
        }
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
333
334
335
336
337
338
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_starred",
        url_path="getStarred",
    )
339
340
    def get_starred(self, request, *args, **kwargs):
        favorites = request.user.track_favorites.all()
Eliot Berriot's avatar
Eliot Berriot committed
341
        data = {"starred": {"song": serializers.get_starred_tracks_data(favorites)}}
342
343
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
344
345
346
347
348
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_album_list2",
        url_path="getAlbumList2",
Eliot Berriot's avatar
Eliot Berriot committed
349
    )
350
    def get_album_list2(self, request, *args, **kwargs):
351
352
353
354
355
356
357
358
        queryset = (
            music_models.Album.objects.exclude(
                moderation_filters.get_filtered_content_query(
                    moderation_filters.USER_FILTER_CONFIG["ALBUM"], request.user
                )
            )
            .with_tracks_count()
            .order_by("artist__name")
359
        )
360
361
362
        data = request.GET or request.POST
        filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
        queryset = filterset.qs
363
364
365
        actor = utils.get_actor_from_request(request)
        queryset = queryset.playable_by(actor)

366
        try:
Eliot Berriot's avatar
Eliot Berriot committed
367
            offset = int(data["offset"])
368
369
370
371
        except (TypeError, KeyError, ValueError):
            offset = 0

        try:
Eliot Berriot's avatar
Eliot Berriot committed
372
            size = int(data["size"])
373
374
375
376
        except (TypeError, KeyError, ValueError):
            size = 50

        size = min(size, 500)
377
        queryset = queryset[offset : offset + size]
Eliot Berriot's avatar
Eliot Berriot committed
378
        data = {"albumList2": {"album": serializers.get_album_list2_data(queryset)}}
379
380
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
381
382
383
    @action(
        detail=False, methods=["get", "post"], url_name="search3", url_path="search3"
    )
384
385
    def search3(self, request, *args, **kwargs):
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
386
        query = str(data.get("query", "")).replace("*", "")
387
        actor = utils.get_actor_from_request(request)
388
389
        conf = [
            {
Eliot Berriot's avatar
Eliot Berriot committed
390
391
392
393
394
395
                "subsonic": "artist",
                "search_fields": ["name"],
                "queryset": (
                    music_models.Artist.objects.with_albums_count().values(
                        "id", "_albums_count", "name"
                    )
396
                ),
Eliot Berriot's avatar
Eliot Berriot committed
397
                "serializer": lambda qs: [serializers.get_artist_data(a) for a in qs],
398
399
            },
            {
Eliot Berriot's avatar
Eliot Berriot committed
400
401
402
403
404
405
                "subsonic": "album",
                "search_fields": ["title"],
                "queryset": (
                    music_models.Album.objects.with_tracks_count().select_related(
                        "artist"
                    )
406
                ),
Eliot Berriot's avatar
Eliot Berriot committed
407
                "serializer": serializers.get_album_list2_data,
408
409
            },
            {
Eliot Berriot's avatar
Eliot Berriot committed
410
411
412
                "subsonic": "song",
                "search_fields": ["title"],
                "queryset": (
Eliot Berriot's avatar
Eliot Berriot committed
413
414
415
                    music_models.Track.objects.prefetch_related(
                        "uploads"
                    ).select_related("album__artist")
416
                ),
Eliot Berriot's avatar
Eliot Berriot committed
417
                "serializer": serializers.get_song_list_data,
418
419
            },
        ]
Eliot Berriot's avatar
Eliot Berriot committed
420
        payload = {"searchResult3": {}}
421
        for c in conf:
Eliot Berriot's avatar
Eliot Berriot committed
422
423
            offsetKey = "{}Offset".format(c["subsonic"])
            countKey = "{}Count".format(c["subsonic"])
424
425
426
427
428
429
430
431
432
433
434
            try:
                offset = int(data[offsetKey])
            except (TypeError, KeyError, ValueError):
                offset = 0

            try:
                size = int(data[countKey])
            except (TypeError, KeyError, ValueError):
                size = 20

            size = min(size, 100)
Eliot Berriot's avatar
Eliot Berriot committed
435
            queryset = c["queryset"]
436
            if query:
Eliot Berriot's avatar
Eliot Berriot committed
437
438
                queryset = c["queryset"].filter(
                    utils.get_query(query, c["search_fields"])
439
                )
440
            queryset = queryset.playable_by(actor)
441
            queryset = common_utils.order_for_search(queryset, c["search_fields"][0])
442
            queryset = queryset[offset : offset + size]
Eliot Berriot's avatar
Eliot Berriot committed
443
            payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset)
444
        return response.Response(payload)
445

Eliot Berriot's avatar
Eliot Berriot committed
446
447
448
449
450
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_playlists",
        url_path="getPlaylists",
Eliot Berriot's avatar
Eliot Berriot committed
451
    )
452
    def get_playlists(self, request, *args, **kwargs):
453
        qs = get_playlist_qs(request)
454
        data = {
455
            "playlists": {"playlist": [serializers.get_playlist_data(p) for p in qs]}
456
457
458
        }
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
459
460
461
462
463
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_playlist",
        url_path="getPlaylist",
Eliot Berriot's avatar
Eliot Berriot committed
464
    )
465
    @find_object(lambda request: get_playlist_qs(request))
466
    def get_playlist(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
467
468
        playlist = kwargs.pop("obj")
        data = {"playlist": serializers.get_playlist_detail_data(playlist)}
469
470
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
471
472
473
474
475
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="update_playlist",
        url_path="updatePlaylist",
Eliot Berriot's avatar
Eliot Berriot committed
476
477
    )
    @find_object(lambda request: request.user.playlists.all(), field="playlistId")
478
    def update_playlist(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
479
        playlist = kwargs.pop("obj")
480
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
481
        new_name = data.get("name", "")
482
483
        if new_name:
            playlist.name = new_name
Eliot Berriot's avatar
Eliot Berriot committed
484
            playlist.save(update_fields=["name", "modification_date"])
485
        try:
Eliot Berriot's avatar
Eliot Berriot committed
486
            to_remove = int(data["songIndexToRemove"])
487
488
489
490
491
492
493
494
            plt = playlist.playlist_tracks.get(index=to_remove)
        except (TypeError, ValueError, KeyError):
            pass
        except playlists_models.PlaylistTrack.DoesNotExist:
            pass
        else:
            plt.delete(update_indexes=True)

495
        ids = []
Eliot Berriot's avatar
Eliot Berriot committed
496
        for i in data.getlist("songIdToAdd"):
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
            try:
                ids.append(int(i))
            except (TypeError, ValueError):
                pass
        if ids:
            tracks = music_models.Track.objects.filter(pk__in=ids)
            by_id = {t.id: t for t in tracks}
            sorted_tracks = []
            for i in ids:
                try:
                    sorted_tracks.append(by_id[i])
                except KeyError:
                    pass
            if sorted_tracks:
                playlist.insert_many(sorted_tracks)

Eliot Berriot's avatar
Eliot Berriot committed
513
        data = {"status": "ok"}
514
515
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
516
517
518
519
520
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="delete_playlist",
        url_path="deletePlaylist",
Eliot Berriot's avatar
Eliot Berriot committed
521
522
    )
    @find_object(lambda request: request.user.playlists.all())
523
    def delete_playlist(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
524
        playlist = kwargs.pop("obj")
525
        playlist.delete()
Eliot Berriot's avatar
Eliot Berriot committed
526
        data = {"status": "ok"}
527
528
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
529
530
531
532
533
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="create_playlist",
        url_path="createPlaylist",
Eliot Berriot's avatar
Eliot Berriot committed
534
    )
535
536
    def create_playlist(self, request, *args, **kwargs):
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
537
        name = data.get("name", "")
538
        if not name:
Eliot Berriot's avatar
Eliot Berriot committed
539
540
541
542
543
544
            return response.Response(
                {
                    "error": {
                        "code": 10,
                        "message": "Playlist ID or name must be specified.",
                    }
545
                }
Eliot Berriot's avatar
Eliot Berriot committed
546
            )
547

Eliot Berriot's avatar
Eliot Berriot committed
548
        playlist = request.user.playlists.create(name=name)
549
        ids = []
Eliot Berriot's avatar
Eliot Berriot committed
550
        for i in data.getlist("songId"):
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
            try:
                ids.append(int(i))
            except (TypeError, ValueError):
                pass

        if ids:
            tracks = music_models.Track.objects.filter(pk__in=ids)
            by_id = {t.id: t for t in tracks}
            sorted_tracks = []
            for i in ids:
                try:
                    sorted_tracks.append(by_id[i])
                except KeyError:
                    pass
            if sorted_tracks:
                playlist.insert_many(sorted_tracks)
Eliot Berriot's avatar
Eliot Berriot committed
567
568
        playlist = request.user.playlists.with_tracks_count().get(pk=playlist.pk)
        data = {"playlist": serializers.get_playlist_detail_data(playlist)}
569
        return response.Response(data)
570

Eliot Berriot's avatar
Eliot Berriot committed
571
572
573
574
575
576
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_avatar",
        url_path="getAvatar",
    )
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
    @find_object(
        queryset=users_models.User.objects.exclude(avatar=None).exclude(avatar=""),
        model_field="username__iexact",
        field="username",
        cast=str,
    )
    def get_avatar(self, request, *args, **kwargs):
        user = kwargs.pop("obj")
        mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
        path = music_views.get_file_path(user.avatar)
        file_header = mapping[settings.REVERSE_PROXY_TYPE]
        # let the proxy set the content-type
        r = response.Response({}, content_type="")
        r[file_header] = path
        return r

Eliot Berriot's avatar
Eliot Berriot committed
593
594
595
    @action(
        detail=False, methods=["get", "post"], url_name="get_user", url_path="getUser"
    )
596
597
598
599
600
601
602
603
604
605
    @find_object(
        queryset=lambda request: users_models.User.objects.filter(pk=request.user.pk),
        model_field="username__iexact",
        field="username",
        cast=str,
    )
    def get_user(self, request, *args, **kwargs):
        data = {"user": serializers.get_user_detail_data(request.user)}
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
606
607
    @action(
        detail=False,
Eliot Berriot's avatar
Eliot Berriot committed
608
609
610
611
        methods=["get", "post"],
        url_name="get_music_folders",
        url_path="getMusicFolders",
    )
612
    def get_music_folders(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
613
        data = {"musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}}
614
        return response.Response(data)
615

Eliot Berriot's avatar
Eliot Berriot committed
616
617
618
619
620
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_cover_art",
        url_path="getCoverArt",
Eliot Berriot's avatar
Eliot Berriot committed
621
    )
622
623
    def get_cover_art(self, request, *args, **kwargs):
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
624
        id = data.get("id", "")
625
        if not id:
Eliot Berriot's avatar
Eliot Berriot committed
626
627
628
            return response.Response(
                {"error": {"code": 10, "message": "cover art ID must be specified."}}
            )
629

Eliot Berriot's avatar
Eliot Berriot committed
630
        if id.startswith("al-"):
631
            try:
Eliot Berriot's avatar
Eliot Berriot committed
632
633
634
635
636
637
                album_id = int(id.replace("al-", ""))
                album = (
                    music_models.Album.objects.exclude(cover__isnull=True)
                    .exclude(cover="")
                    .get(pk=album_id)
                )
638
            except (TypeError, ValueError, music_models.Album.DoesNotExist):
Eliot Berriot's avatar
Eliot Berriot committed
639
640
641
                return response.Response(
                    {"error": {"code": 70, "message": "cover art not found."}}
                )
642
643
            cover = album.cover
        else:
Eliot Berriot's avatar
Eliot Berriot committed
644
645
646
            return response.Response(
                {"error": {"code": 70, "message": "cover art not found."}}
            )
647

Eliot Berriot's avatar
Eliot Berriot committed
648
        mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
649
650
651
        path = music_views.get_file_path(cover)
        file_header = mapping[settings.REVERSE_PROXY_TYPE]
        # let the proxy set the content-type
Eliot Berriot's avatar
Eliot Berriot committed
652
        r = response.Response({}, content_type="")
653
        r[file_header] = path
654
655
        return r

Eliot Berriot's avatar
Eliot Berriot committed
656
657
658
    @action(
        detail=False, methods=["get", "post"], url_name="scrobble", url_path="scrobble"
    )
659
660
661
    def scrobble(self, request, *args, **kwargs):
        data = request.GET or request.POST
        serializer = serializers.ScrobbleSerializer(
Eliot Berriot's avatar
Eliot Berriot committed
662
663
            data=data, context={"user": request.user}
        )
664
        if not serializer.is_valid():
Eliot Berriot's avatar
Eliot Berriot committed
665
666
667
668
            return response.Response(
                {"error": {"code": 0, "message": "Invalid payload"}}
            )
        if serializer.validated_data["submission"]:
669
670
            listening = serializer.save()
            record.send(listening)
671
        return response.Response({})