Skip to content
Snippets Groups Projects
views.py 20.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • from django.utils import timezone
    
    from rest_framework import exceptions
    from rest_framework import permissions as rest_permissions
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from rest_framework import renderers, response, viewsets
    
    from rest_framework.decorators import list_route
    from rest_framework.serializers import ValidationError
    
    
    from funkwhale_api.activity import record
    
    from funkwhale_api.common import preferences
    
    from funkwhale_api.favorites.models import TrackFavorite
    
    from funkwhale_api.music import models as music_models
    
    from funkwhale_api.music import utils
    
    from funkwhale_api.music import views as music_views
    
    from funkwhale_api.playlists import models as playlists_models
    
    from funkwhale_api.users import models as users_models
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from . import authentication, filters, negotiation, serializers
    
    def find_object(
        queryset, model_field="pk", field="id", cast=int, filter_playable=False
    ):
    
        def decorator(func):
            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
                    return response.Response(
                        {
                            "error": {
                                "code": 10,
                                "message": "required parameter '{}' not present".format(
                                    field
                                ),
                            }
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    )
    
                except (ValueError, TypeError, ValidationError):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    return response.Response(
                        {
                            "error": {
                                "code": 0,
                                "message": 'For input string "{}"'.format(raw_value),
                            }
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                if hasattr(qs, "__call__"):
    
    
                if filter_playable:
                    actor = utils.get_actor_from_request(request)
                    qs = qs.playable_by(actor).distinct()
    
    
                    obj = qs.get(**{model_field: value})
                except qs.model.DoesNotExist:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    return response.Response(
                        {
                            "error": {
                                "code": 70,
                                "message": "{} not found".format(
                                    qs.model.__class__.__name__
                                ),
                            }
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    )
                kwargs["obj"] = obj
    
                return func(self, request, *args, **kwargs)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
        return decorator
    
    
    class SubsonicViewSet(viewsets.GenericViewSet):
        content_negotiation_class = negotiation.SubsonicContentNegociation
        authentication_classes = [authentication.SubsonicAuthentication]
        permissions_classes = [rest_permissions.IsAuthenticated]
    
    
        def dispatch(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if not preferences.get("subsonic__enabled"):
    
                r = response.Response({}, status=405)
                r.accepted_renderer = renderers.JSONRenderer()
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                r.accepted_media_type = "application/json"
    
                r.renderer_context = {}
                return r
            return super().dispatch(request, *args, **kwargs)
    
    
        def handle_exception(self, exc):
            # subsonic API sends 200 status code with custom error
            # codes in the payload
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            mapping = {exceptions.AuthenticationFailed: (40, "Wrong username or password.")}
            payload = {"status": "failed"}
    
            if exc.__class__ in mapping:
    
                return super().handle_exception(exc)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            payload["error"] = {"code": code, "message": message}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], permission_classes=[])
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            data = {"status": "ok", "version": "1.16.0"}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"],
            url_name="get_license",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            url_path="getLicense",
        )
    
        def get_license(self, request, *args, **kwargs):
            now = timezone.now()
            data = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "status": "ok",
                "version": "1.16.0",
    
                "type": "funkwhale",
                "funkwhale-version": funkwhale_api.__version__,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "license": {
                    "valid": "true",
                    "email": "valid@valid.license",
                    "licenseExpires": now + datetime.timedelta(days=365),
                },
    
            }
            return response.Response(data, status=200)
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="get_artists", url_path="getArtists")
    
        def get_artists(self, request, *args, **kwargs):
    
            artists = music_models.Artist.objects.all().playable_by(
                utils.get_actor_from_request(request)
            )
    
            data = serializers.GetArtistsSerializer(artists).data
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            payload = {"artists": data}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="get_indexes", url_path="getIndexes")
    
        def get_indexes(self, request, *args, **kwargs):
    
            artists = music_models.Artist.objects.all().playable_by(
                utils.get_actor_from_request(request)
            )
    
            data = serializers.GetArtistsSerializer(artists).data
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            payload = {"indexes": data}
    
    
            return response.Response(payload, status=200)
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="get_artist", url_path="getArtist")
    
        @find_object(music_models.Artist.objects.all(), filter_playable=True)
    
        def get_artist(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            artist = kwargs.pop("obj")
    
            data = serializers.GetArtistSerializer(artist).data
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            payload = {"artist": data}
    
        @list_route(methods=["get", "post"], url_name="get_song", url_path="getSong")
    
        @find_object(music_models.Track.objects.all(), filter_playable=True)
    
        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
            methods=["get", "post"], url_name="get_artist_info2", url_path="getArtistInfo2"
        )
    
        @find_object(music_models.Artist.objects.all(), filter_playable=True)
    
        def get_artist_info2(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            payload = {"artist-info2": {}}
    
    
            return response.Response(payload, status=200)
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="get_album", url_path="getAlbum")
    
        @find_object(
            music_models.Album.objects.select_related("artist"), filter_playable=True
        )
    
        def get_album(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            album = kwargs.pop("obj")
    
            data = serializers.GetAlbumSerializer(album).data
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            payload = {"album": data}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="stream", url_path="stream")
    
        @find_object(music_models.Track.objects.all(), filter_playable=True)
    
        def stream(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            track = kwargs.pop("obj")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            queryset = track.uploads.select_related("track__album__artist", "track__artist")
            upload = queryset.first()
            if not upload:
    
                return response.Response(status=404)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            return music_views.handle_serve(upload=upload, user=request.user)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="star", url_path="star")
        @find_object(music_models.Track.objects.all())
    
        def star(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            track = kwargs.pop("obj")
    
            TrackFavorite.add(user=request.user, track=track)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            return response.Response({"status": "ok"})
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="unstar", url_path="unstar")
        @find_object(music_models.Track.objects.all())
    
        def unstar(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            track = kwargs.pop("obj")
    
            request.user.track_favorites.filter(track=track).delete()
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            return response.Response({"status": "ok"})
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"], url_name="get_starred2", url_path="getStarred2"
        )
    
        def get_starred2(self, request, *args, **kwargs):
            favorites = request.user.track_favorites.all()
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}}
    
            return response.Response(data)
    
    
        @list_route(
            methods=["get", "post"], url_name="get_random_songs", url_path="getRandomSongs"
        )
        def get_random_songs(self, request, *args, **kwargs):
            data = request.GET or request.POST
            actor = utils.get_actor_from_request(request)
            queryset = music_models.Track.objects.all()
            queryset = queryset.playable_by(actor)
            try:
                size = int(data["size"])
            except (TypeError, KeyError, ValueError):
                size = 50
    
            queryset = queryset.playable_by(actor).prefetch_related('uploads').order_by("?")[:size]
            data = {
                "randomSongs": {
                    "song": serializers.GetSongSerializer(queryset, many=True).data
                }
            }
            return response.Response(data)
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="get_starred", url_path="getStarred")
    
        def get_starred(self, request, *args, **kwargs):
            favorites = request.user.track_favorites.all()
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            data = {"starred": {"song": serializers.get_starred_tracks_data(favorites)}}
    
            return response.Response(data)
    
        @list_route(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"], url_name="get_album_list2", url_path="getAlbumList2"
        )
    
        def get_album_list2(self, request, *args, **kwargs):
    
            queryset = music_models.Album.objects.with_tracks_count().order_by(
                "artist__name"
            )
    
            data = request.GET or request.POST
            filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
            queryset = filterset.qs
    
            actor = utils.get_actor_from_request(request)
            queryset = queryset.playable_by(actor)
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                offset = int(data["offset"])
    
            except (TypeError, KeyError, ValueError):
                offset = 0
    
            try:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                size = int(data["size"])
    
            except (TypeError, KeyError, ValueError):
                size = 50
    
            size = min(size, 500)
    
            queryset = queryset[offset : offset + size]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            data = {"albumList2": {"album": serializers.get_album_list2_data(queryset)}}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="search3", url_path="search3")
    
        def search3(self, request, *args, **kwargs):
            data = request.GET or request.POST
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            query = str(data.get("query", "")).replace("*", "")
    
            actor = utils.get_actor_from_request(request)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "subsonic": "artist",
                    "search_fields": ["name"],
                    "queryset": (
                        music_models.Artist.objects.with_albums_count().values(
                            "id", "_albums_count", "name"
                        )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "serializer": lambda qs: [serializers.get_artist_data(a) for a in qs],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "subsonic": "album",
                    "search_fields": ["title"],
                    "queryset": (
                        music_models.Album.objects.with_tracks_count().select_related(
                            "artist"
                        )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "serializer": serializers.get_album_list2_data,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "subsonic": "song",
                    "search_fields": ["title"],
                    "queryset": (
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                        music_models.Track.objects.prefetch_related(
                            "uploads"
                        ).select_related("album__artist")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "serializer": serializers.get_song_list_data,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            payload = {"searchResult3": {}}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                offsetKey = "{}Offset".format(c["subsonic"])
                countKey = "{}Count".format(c["subsonic"])
    
                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
                queryset = c["queryset"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    queryset = c["queryset"].filter(
                        utils.get_query(query, c["search_fields"])
    
                queryset = queryset.playable_by(actor)
    
                queryset = queryset[offset : offset + size]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset)
    
            return response.Response(payload)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"], url_name="get_playlists", url_path="getPlaylists"
        )
    
        def get_playlists(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            playlists = request.user.playlists.with_tracks_count().select_related("user")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "playlists": {
                    "playlist": [serializers.get_playlist_data(p) for p in playlists]
    
                }
            }
            return response.Response(data)
    
        @list_route(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"], url_name="get_playlist", url_path="getPlaylist"
        )
        @find_object(playlists_models.Playlist.objects.with_tracks_count())
    
        def get_playlist(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            playlist = kwargs.pop("obj")
            data = {"playlist": serializers.get_playlist_detail_data(playlist)}
    
            return response.Response(data)
    
        @list_route(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"], url_name="update_playlist", url_path="updatePlaylist"
        )
        @find_object(lambda request: request.user.playlists.all(), field="playlistId")
    
        def update_playlist(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            playlist = kwargs.pop("obj")
    
            data = request.GET or request.POST
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            new_name = data.get("name", "")
    
            if new_name:
                playlist.name = new_name
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                playlist.save(update_fields=["name", "modification_date"])
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                to_remove = int(data["songIndexToRemove"])
    
                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)
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            for i in data.getlist("songIdToAdd"):
    
                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
            data = {"status": "ok"}
    
            return response.Response(data)
    
        @list_route(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"], url_name="delete_playlist", url_path="deletePlaylist"
        )
        @find_object(lambda request: request.user.playlists.all())
    
        def delete_playlist(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            playlist = kwargs.pop("obj")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            data = {"status": "ok"}
    
            return response.Response(data)
    
        @list_route(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"], url_name="create_playlist", url_path="createPlaylist"
        )
    
        def create_playlist(self, request, *args, **kwargs):
            data = request.GET or request.POST
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            name = data.get("name", "")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                return response.Response(
                    {
                        "error": {
                            "code": 10,
                            "message": "Playlist ID or name must be specified.",
                        }
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            playlist = request.user.playlists.create(name=name)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            for i in data.getlist("songId"):
    
                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
            playlist = request.user.playlists.with_tracks_count().get(pk=playlist.pk)
            data = {"playlist": serializers.get_playlist_detail_data(playlist)}
    
            return response.Response(data)
    
        @list_route(methods=["get", "post"], url_name="get_avatar", url_path="getAvatar")
        @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
    
    
        @list_route(methods=["get", "post"], url_name="get_user", url_path="getUser")
        @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
            methods=["get", "post"],
            url_name="get_music_folders",
            url_path="getMusicFolders",
        )
    
        def get_music_folders(self, request, *args, **kwargs):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            data = {"musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}}
    
            return response.Response(data)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            methods=["get", "post"], url_name="get_cover_art", url_path="getCoverArt"
        )
    
        def get_cover_art(self, request, *args, **kwargs):
            data = request.GET or request.POST
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            id = data.get("id", "")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                return response.Response(
                    {"error": {"code": 10, "message": "cover art ID must be specified."}}
                )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if id.startswith("al-"):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    album_id = int(id.replace("al-", ""))
                    album = (
                        music_models.Album.objects.exclude(cover__isnull=True)
                        .exclude(cover="")
                        .get(pk=album_id)
                    )
    
                except (TypeError, ValueError, music_models.Album.DoesNotExist):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    return response.Response(
                        {"error": {"code": 70, "message": "cover art not found."}}
                    )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                return response.Response(
                    {"error": {"code": 70, "message": "cover art not found."}}
                )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
    
            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
            r = response.Response({}, content_type="")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        @list_route(methods=["get", "post"], url_name="scrobble", url_path="scrobble")
    
        def scrobble(self, request, *args, **kwargs):
            data = request.GET or request.POST
            serializer = serializers.ScrobbleSerializer(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                data=data, context={"user": request.user}
            )
    
            if not serializer.is_valid():
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                return response.Response(
                    {"error": {"code": 0, "message": "Invalid payload"}}
                )
            if serializer.validated_data["submission"]:
    
                listening = serializer.save()
                record.send(listening)
    
            return response.Response({})