views.py 27.6 KB
Newer Older
1
2
3
"""
Documentation of Subsonic API can be found at http://www.subsonic.org/pages/api.jsp
"""
4
import datetime
Eliot Berriot's avatar
Eliot Berriot committed
5
import functools
6

7
from django.conf import settings
8
9
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q
10
from django.utils import timezone
11
12
from rest_framework import exceptions
from rest_framework import permissions as rest_permissions
Eliot Berriot's avatar
Eliot Berriot committed
13
from rest_framework import renderers, response, viewsets
Eliot Berriot's avatar
Eliot Berriot committed
14
from rest_framework.decorators import action
15
16
from rest_framework.serializers import ValidationError

17
import funkwhale_api
18
from funkwhale_api.activity import record
19
from funkwhale_api.common import fields, preferences, utils as common_utils
20
from funkwhale_api.favorites.models import TrackFavorite
21
from funkwhale_api.moderation import filters as moderation_filters
22
from funkwhale_api.music import models as music_models
23
from funkwhale_api.music import utils
24
from funkwhale_api.music import views as music_views
25
from funkwhale_api.playlists import models as playlists_models
26
from funkwhale_api.tags import models as tags_models
27
from funkwhale_api.users import models as users_models
28

Eliot Berriot's avatar
Eliot Berriot committed
29
from . import authentication, filters, negotiation, serializers
30
31


32
33
34
def find_object(
    queryset, model_field="pk", field="id", cast=int, filter_playable=False
):
35
    def decorator(func):
Eliot Berriot's avatar
Eliot Berriot committed
36
        @functools.wraps(func)
37
38
39
40
41
        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
42
43
44
45
46
47
48
49
                return response.Response(
                    {
                        "error": {
                            "code": 10,
                            "message": "required parameter '{}' not present".format(
                                field
                            ),
                        }
50
                    }
Eliot Berriot's avatar
Eliot Berriot committed
51
                )
52
53
            try:
                value = cast(raw_value)
54
            except (ValueError, TypeError, ValidationError):
Eliot Berriot's avatar
Eliot Berriot committed
55
56
57
58
59
60
                return response.Response(
                    {
                        "error": {
                            "code": 0,
                            "message": 'For input string "{}"'.format(raw_value),
                        }
61
                    }
Eliot Berriot's avatar
Eliot Berriot committed
62
                )
63
            qs = queryset
Eliot Berriot's avatar
Eliot Berriot committed
64
            if hasattr(qs, "__call__"):
65
                qs = qs(request)
66
67
68

            if filter_playable:
                actor = utils.get_actor_from_request(request)
69
                qs = qs.playable_by(actor)
70

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

85
        return inner
Eliot Berriot's avatar
Eliot Berriot committed
86

87
88
89
    return decorator


90
91
92
93
94
95
96
97
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")


98
99
100
class SubsonicViewSet(viewsets.GenericViewSet):
    content_negotiation_class = negotiation.SubsonicContentNegociation
    authentication_classes = [authentication.SubsonicAuthentication]
101
    permission_classes = [rest_permissions.IsAuthenticated]
102

103
    def dispatch(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
104
        if not preferences.get("subsonic__enabled"):
105
106
            r = response.Response({}, status=405)
            r.accepted_renderer = renderers.JSONRenderer()
Eliot Berriot's avatar
Eliot Berriot committed
107
            r.accepted_media_type = "application/json"
108
109
110
111
            r.renderer_context = {}
            return r
        return super().dispatch(request, *args, **kwargs)

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

        return response.Response(payload, status=200)

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

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

Eliot Berriot's avatar
Eliot Berriot committed
155
156
157
158
159
160
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_artists",
        url_path="getArtists",
    )
161
    def get_artists(self, request, *args, **kwargs):
162
163
164
165
166
167
168
169
        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))
170
        )
171
        data = serializers.GetArtistsSerializer(artists).data
Eliot Berriot's avatar
Eliot Berriot committed
172
        payload = {"artists": data}
173
174
175

        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
176
177
178
179
180
181
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_indexes",
        url_path="getIndexes",
    )
182
    def get_indexes(self, request, *args, **kwargs):
183
184
185
186
187
188
189
190
        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))
191
        )
192
        data = serializers.GetArtistsSerializer(artists).data
Eliot Berriot's avatar
Eliot Berriot committed
193
        payload = {"indexes": data}
194
195
196

        return response.Response(payload, status=200)

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

        return response.Response(payload, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
211
212
213
    @action(
        detail=False, methods=["get", "post"], url_name="get_song", url_path="getSong"
    )
214
    @find_object(music_models.Track.objects.all(), filter_playable=True)
215
216
217
218
219
220
221
    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
222
223
224
225
226
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_artist_info2",
        url_path="getArtistInfo2",
Eliot Berriot's avatar
Eliot Berriot committed
227
    )
228
    @find_object(music_models.Artist.objects.all(), filter_playable=True)
229
    def get_artist_info2(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
230
        payload = {"artist-info2": {}}
231
232
233

        return response.Response(payload, status=200)

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

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

256
257
258
259
260
261
262
263
        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
264

265
        format = data.get("format") or None
266
267
268
269
270
271
272
273
        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

274
        return music_views.handle_serve(
275
276
277
278
279
280
281
            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,
282
        )
283

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

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

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

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

Eliot Berriot's avatar
Eliot Berriot committed
329
330
331
        queryset = (
            queryset.playable_by(actor).prefetch_related("uploads").order_by("?")[:size]
        )
332
333
334
335
336
337
338
        data = {
            "randomSongs": {
                "song": serializers.GetSongSerializer(queryset, many=True).data
            }
        }
        return response.Response(data)

339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_songs_by_genre",
        url_path="getSongsByGenre",
    )
    def get_songs_by_genre(self, request, *args, **kwargs):
        data = request.GET or request.POST
        actor = utils.get_actor_from_request(request)
        queryset = music_models.Track.objects.all().exclude(
            moderation_filters.get_filtered_content_query(
                moderation_filters.USER_FILTER_CONFIG["TRACK"], request.user
            )
        )
        queryset = queryset.playable_by(actor)
        try:
            size = int(
                data["count"]
            )  # yep. Some endpoints have size, other have count…
        except (TypeError, KeyError, ValueError):
            size = 50

        genre = data.get("genre")
        queryset = (
            queryset.playable_by(actor)
            .filter(
                Q(tagged_items__tag__name=genre)
                | Q(artist__tagged_items__tag__name=genre)
                | Q(album__artist__tagged_items__tag__name=genre)
                | Q(album__tagged_items__tag__name=genre)
            )
            .prefetch_related("uploads")
            .distinct()
            .order_by("-creation_date")[:size]
        )
        data = {
            "songsByGenre": {
                "song": serializers.GetSongSerializer(queryset, many=True).data
            }
        }
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
381
382
383
384
385
386
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_starred",
        url_path="getStarred",
    )
387
388
    def get_starred(self, request, *args, **kwargs):
        favorites = request.user.track_favorites.all()
Eliot Berriot's avatar
Eliot Berriot committed
389
        data = {"starred": {"song": serializers.get_starred_tracks_data(favorites)}}
390
391
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
392
393
394
395
396
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_album_list2",
        url_path="getAlbumList2",
Eliot Berriot's avatar
Eliot Berriot committed
397
    )
398
    def get_album_list2(self, request, *args, **kwargs):
399
400
401
402
403
404
405
406
        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")
407
        )
408
409
410
        data = request.GET or request.POST
        filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
        queryset = filterset.qs
411
412
        actor = utils.get_actor_from_request(request)
        queryset = queryset.playable_by(actor)
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
        type = data.get("type", "alphabeticalByArtist")

        if type == "alphabeticalByArtist":
            queryset = queryset.order_by("artist__name")
        elif type == "random":
            queryset = queryset.order_by("?")
        elif type == "alphabeticalByName" or not type:
            queryset = queryset.order_by("artist__title")
        elif type == "recent" or not type:
            queryset = queryset.exclude(release_date__in=["", None]).order_by(
                "-release_date"
            )
        elif type == "newest" or not type:
            queryset = queryset.order_by("-creation_date")
        elif type == "byGenre" and data.get("genre"):
            genre = data.get("genre")
            queryset = queryset.filter(
                Q(tagged_items__tag__name=genre)
                | Q(artist__tagged_items__tag__name=genre)
            )
433
434
435
436
437
438
        elif type == "byYear":
            try:
                boundaries = [
                    int(data.get("fromYear", 0)),
                    int(data.get("toYear", 99999999)),
                ]
439

440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
            except (TypeError, ValueError):
                return response.Response(
                    {
                        "error": {
                            "code": 10,
                            "message": "Invalid fromYear or toYear parameter",
                        }
                    }
                )
            # because, yeah, the specification explicitly state that fromYear can be greater
            # than toYear, to indicate reverse ordering…
            # http://www.subsonic.org/pages/api.jsp#getAlbumList2
            from_year = min(boundaries)
            to_year = max(boundaries)
            queryset = queryset.filter(
                release_date__year__gte=from_year, release_date__year__lte=to_year
            )
            if boundaries[0] <= boundaries[1]:
                queryset = queryset.order_by("release_date")
            else:
                queryset = queryset.order_by("-release_date")
461
        try:
Eliot Berriot's avatar
Eliot Berriot committed
462
            offset = int(data["offset"])
463
464
465
466
        except (TypeError, KeyError, ValueError):
            offset = 0

        try:
Eliot Berriot's avatar
Eliot Berriot committed
467
            size = int(data["size"])
468
469
470
471
        except (TypeError, KeyError, ValueError):
            size = 50

        size = min(size, 500)
472
        queryset = queryset[offset : offset + size]
Eliot Berriot's avatar
Eliot Berriot committed
473
        data = {"albumList2": {"album": serializers.get_album_list2_data(queryset)}}
474
475
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
476
477
478
    @action(
        detail=False, methods=["get", "post"], url_name="search3", url_path="search3"
    )
479
480
    def search3(self, request, *args, **kwargs):
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
481
        query = str(data.get("query", "")).replace("*", "")
482
        actor = utils.get_actor_from_request(request)
483
484
        conf = [
            {
Eliot Berriot's avatar
Eliot Berriot committed
485
486
487
488
489
490
                "subsonic": "artist",
                "search_fields": ["name"],
                "queryset": (
                    music_models.Artist.objects.with_albums_count().values(
                        "id", "_albums_count", "name"
                    )
491
                ),
Eliot Berriot's avatar
Eliot Berriot committed
492
                "serializer": lambda qs: [serializers.get_artist_data(a) for a in qs],
493
494
            },
            {
Eliot Berriot's avatar
Eliot Berriot committed
495
496
497
498
499
500
                "subsonic": "album",
                "search_fields": ["title"],
                "queryset": (
                    music_models.Album.objects.with_tracks_count().select_related(
                        "artist"
                    )
501
                ),
Eliot Berriot's avatar
Eliot Berriot committed
502
                "serializer": serializers.get_album_list2_data,
503
504
            },
            {
Eliot Berriot's avatar
Eliot Berriot committed
505
506
507
                "subsonic": "song",
                "search_fields": ["title"],
                "queryset": (
Eliot Berriot's avatar
Eliot Berriot committed
508
509
510
                    music_models.Track.objects.prefetch_related(
                        "uploads"
                    ).select_related("album__artist")
511
                ),
Eliot Berriot's avatar
Eliot Berriot committed
512
                "serializer": serializers.get_song_list_data,
513
514
            },
        ]
Eliot Berriot's avatar
Eliot Berriot committed
515
        payload = {"searchResult3": {}}
516
        for c in conf:
Eliot Berriot's avatar
Eliot Berriot committed
517
518
            offsetKey = "{}Offset".format(c["subsonic"])
            countKey = "{}Count".format(c["subsonic"])
519
520
521
522
523
524
525
526
527
528
529
            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
530
            queryset = c["queryset"]
531
            if query:
Eliot Berriot's avatar
Eliot Berriot committed
532
533
                queryset = c["queryset"].filter(
                    utils.get_query(query, c["search_fields"])
534
                )
535
            queryset = queryset.playable_by(actor)
536
            queryset = common_utils.order_for_search(queryset, c["search_fields"][0])
537
            queryset = queryset[offset : offset + size]
Eliot Berriot's avatar
Eliot Berriot committed
538
            payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset)
539
        return response.Response(payload)
540

Eliot Berriot's avatar
Eliot Berriot committed
541
542
543
544
545
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_playlists",
        url_path="getPlaylists",
Eliot Berriot's avatar
Eliot Berriot committed
546
    )
547
    def get_playlists(self, request, *args, **kwargs):
548
        qs = get_playlist_qs(request)
549
        data = {
550
            "playlists": {"playlist": [serializers.get_playlist_data(p) for p in qs]}
551
552
553
        }
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
554
555
556
557
558
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_playlist",
        url_path="getPlaylist",
Eliot Berriot's avatar
Eliot Berriot committed
559
    )
560
    @find_object(lambda request: get_playlist_qs(request))
561
    def get_playlist(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
562
563
        playlist = kwargs.pop("obj")
        data = {"playlist": serializers.get_playlist_detail_data(playlist)}
564
565
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
566
567
568
569
570
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="update_playlist",
        url_path="updatePlaylist",
Eliot Berriot's avatar
Eliot Berriot committed
571
572
    )
    @find_object(lambda request: request.user.playlists.all(), field="playlistId")
573
    def update_playlist(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
574
        playlist = kwargs.pop("obj")
575
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
576
        new_name = data.get("name", "")
577
578
        if new_name:
            playlist.name = new_name
Eliot Berriot's avatar
Eliot Berriot committed
579
            playlist.save(update_fields=["name", "modification_date"])
580
        try:
Eliot Berriot's avatar
Eliot Berriot committed
581
            to_remove = int(data["songIndexToRemove"])
582
583
584
585
586
587
588
589
            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)

590
        ids = []
Eliot Berriot's avatar
Eliot Berriot committed
591
        for i in data.getlist("songIdToAdd"):
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
            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
608
        data = {"status": "ok"}
609
610
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
611
612
613
614
615
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="delete_playlist",
        url_path="deletePlaylist",
Eliot Berriot's avatar
Eliot Berriot committed
616
617
    )
    @find_object(lambda request: request.user.playlists.all())
618
    def delete_playlist(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
619
        playlist = kwargs.pop("obj")
620
        playlist.delete()
Eliot Berriot's avatar
Eliot Berriot committed
621
        data = {"status": "ok"}
622
623
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
624
625
626
627
628
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="create_playlist",
        url_path="createPlaylist",
Eliot Berriot's avatar
Eliot Berriot committed
629
    )
630
631
    def create_playlist(self, request, *args, **kwargs):
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
632
        name = data.get("name", "")
633
        if not name:
Eliot Berriot's avatar
Eliot Berriot committed
634
635
636
637
638
639
            return response.Response(
                {
                    "error": {
                        "code": 10,
                        "message": "Playlist ID or name must be specified.",
                    }
640
                }
Eliot Berriot's avatar
Eliot Berriot committed
641
            )
642

Eliot Berriot's avatar
Eliot Berriot committed
643
        playlist = request.user.playlists.create(name=name)
644
        ids = []
Eliot Berriot's avatar
Eliot Berriot committed
645
        for i in data.getlist("songId"):
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
            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
662
663
        playlist = request.user.playlists.with_tracks_count().get(pk=playlist.pk)
        data = {"playlist": serializers.get_playlist_detail_data(playlist)}
664
        return response.Response(data)
665

Eliot Berriot's avatar
Eliot Berriot committed
666
667
668
669
670
671
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_avatar",
        url_path="getAvatar",
    )
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
    @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
688
689
690
    @action(
        detail=False, methods=["get", "post"], url_name="get_user", url_path="getUser"
    )
691
692
693
694
695
696
697
698
699
700
    @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
701
702
    @action(
        detail=False,
Eliot Berriot's avatar
Eliot Berriot committed
703
704
705
706
        methods=["get", "post"],
        url_name="get_music_folders",
        url_path="getMusicFolders",
    )
707
    def get_music_folders(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
708
        data = {"musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}}
709
        return response.Response(data)
710

Eliot Berriot's avatar
Eliot Berriot committed
711
712
713
714
715
    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_cover_art",
        url_path="getCoverArt",
Eliot Berriot's avatar
Eliot Berriot committed
716
    )
717
718
    def get_cover_art(self, request, *args, **kwargs):
        data = request.GET or request.POST
Eliot Berriot's avatar
Eliot Berriot committed
719
        id = data.get("id", "")
720
        if not id:
Eliot Berriot's avatar
Eliot Berriot committed
721
722
723
            return response.Response(
                {"error": {"code": 10, "message": "cover art ID must be specified."}}
            )
724

Eliot Berriot's avatar
Eliot Berriot committed
725
        if id.startswith("al-"):
726
            try:
Eliot Berriot's avatar
Eliot Berriot committed
727
728
729
730
731
732
                album_id = int(id.replace("al-", ""))
                album = (
                    music_models.Album.objects.exclude(cover__isnull=True)
                    .exclude(cover="")
                    .get(pk=album_id)
                )
733
            except (TypeError, ValueError, music_models.Album.DoesNotExist):
Eliot Berriot's avatar
Eliot Berriot committed
734
735
736
                return response.Response(
                    {"error": {"code": 70, "message": "cover art not found."}}
                )
737
738
            cover = album.cover
        else:
Eliot Berriot's avatar
Eliot Berriot committed
739
740
741
            return response.Response(
                {"error": {"code": 70, "message": "cover art not found."}}
            )
742

Eliot Berriot's avatar
Eliot Berriot committed
743
        mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
744
745
746
        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
747
        r = response.Response({}, content_type="")
748
        r[file_header] = path
749
750
        return r

Eliot Berriot's avatar
Eliot Berriot committed
751
752
753
    @action(
        detail=False, methods=["get", "post"], url_name="scrobble", url_path="scrobble"
    )
754
755
756
    def scrobble(self, request, *args, **kwargs):
        data = request.GET or request.POST
        serializer = serializers.ScrobbleSerializer(
Eliot Berriot's avatar
Eliot Berriot committed
757
758
            data=data, context={"user": request.user}
        )
759
        if not serializer.is_valid():
Eliot Berriot's avatar
Eliot Berriot committed
760
761
762
763
            return response.Response(
                {"error": {"code": 0, "message": "Invalid payload"}}
            )
        if serializer.validated_data["submission"]:
764
765
            listening = serializer.save()
            record.send(listening)
766
        return response.Response({})
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792

    @action(
        detail=False,
        methods=["get", "post"],
        url_name="get_genres",
        url_path="getGenres",
    )
    def get_genres(self, request, *args, **kwargs):
        album_ct = ContentType.objects.get_for_model(music_models.Album)
        track_ct = ContentType.objects.get_for_model(music_models.Track)
        queryset = (
            tags_models.Tag.objects.annotate(
                _albums_count=Count(
                    "tagged_items", filter=Q(tagged_items__content_type=album_ct)
                ),
                _tracks_count=Count(
                    "tagged_items", filter=Q(tagged_items__content_type=track_ct)
                ),
            )
            .exclude(_tracks_count=0, _albums_count=0)
            .order_by("name")
        )
        data = {
            "genres": {"genre": [serializers.get_genre_data(tag) for tag in queryset]}
        }
        return response.Response(data)