From 0f792bf75cfa9ea0be1b73f2863706c38e66cd35 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Fri, 1 Jun 2018 23:59:08 +0200
Subject: [PATCH] Fix #260: Implemented scrobble endpoint of subsonic API

---
 api/funkwhale_api/subsonic/serializers.py | 16 ++++++++++++++++
 api/funkwhale_api/subsonic/views.py       | 23 +++++++++++++++++++++--
 api/tests/subsonic/test_serializers.py    | 19 +++++++++++++++++++
 api/tests/subsonic/test_views.py          | 14 ++++++++++++++
 changes/changelog.d/260.enhancement       |  2 ++
 5 files changed, 72 insertions(+), 2 deletions(-)
 create mode 100644 changes/changelog.d/260.enhancement

diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py
index f63ad5d2..97cdbcfc 100644
--- a/api/funkwhale_api/subsonic/serializers.py
+++ b/api/funkwhale_api/subsonic/serializers.py
@@ -4,6 +4,7 @@ from django.db.models import functions, Count
 
 from rest_framework import serializers
 
+from funkwhale_api.history import models as history_models
 from funkwhale_api.music import models as music_models
 
 
@@ -228,3 +229,18 @@ def get_music_directory_data(artist):
             td['size'] = tf.size
         data['child'].append(td)
     return data
+
+
+class ScrobbleSerializer(serializers.Serializer):
+    submission = serializers.BooleanField(default=True, required=False)
+    id = serializers.PrimaryKeyRelatedField(
+        queryset=music_models.Track.objects.annotate(
+            files_count=Count('files')
+        ).filter(files_count__gt=0)
+    )
+
+    def create(self, data):
+        return history_models.Listening.objects.create(
+            user=self.context['user'],
+            track=data['id'],
+        )
diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py
index 87c9f727..dbc31ec5 100644
--- a/api/funkwhale_api/subsonic/views.py
+++ b/api/funkwhale_api/subsonic/views.py
@@ -519,7 +519,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
                     'message': 'cover art ID must be specified.'
                 }
             })
-        
+
         if id.startswith('al-'):
             try:
                 album_id = int(id.replace('al-', ''))
@@ -551,4 +551,23 @@ class SubsonicViewSet(viewsets.GenericViewSet):
         # let the proxy set the content-type
         r = response.Response({}, content_type='')
         r[file_header] = path
-        return r
\ No newline at end of file
+        return r
+
+    @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(
+            data=data, context={'user': request.user})
+        if not serializer.is_valid():
+            return response.Response({
+                'error': {
+                    'code': 0,
+                    'message': 'Invalid payload'
+                }
+            })
+        if serializer.validated_data['submission']:
+            serializer.save()
+        return response.Response({})
diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py
index 081b669c..6b9ec232 100644
--- a/api/tests/subsonic/test_serializers.py
+++ b/api/tests/subsonic/test_serializers.py
@@ -214,3 +214,22 @@ def test_directory_serializer_artist(factories):
     }
     data = serializers.get_music_directory_data(artist)
     assert data == expected
+
+
+def test_scrobble_serializer(factories):
+    tf = factories['music.TrackFile']()
+    track = tf.track
+    user = factories['users.User']()
+    payload = {
+        'id': track.pk,
+        'submission': True,
+    }
+    serializer = serializers.ScrobbleSerializer(
+        data=payload, context={'user': user})
+
+    assert serializer.is_valid(raise_exception=True)
+
+    listening = serializer.save()
+
+    assert listening.user == user
+    assert listening.track == track
diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py
index 65c2ad95..52e410e5 100644
--- a/api/tests/subsonic/test_views.py
+++ b/api/tests/subsonic/test_views.py
@@ -404,3 +404,17 @@ def test_get_cover_art_album(factories, logged_in_api_client):
     assert response['X-Accel-Redirect'] == music_views.get_file_path(
         album.cover
     ).decode('utf-8')
+
+
+def test_scrobble(factories, logged_in_api_client):
+    tf = factories['music.TrackFile']()
+    track = tf.track
+    url = reverse('api:subsonic-scrobble')
+    assert url.endswith('scrobble') is True
+    response = logged_in_api_client.get(
+        url, {'id': track.pk, 'submission': True})
+
+    assert response.status_code == 200
+
+    l = logged_in_api_client.user.listenings.latest('id')
+    assert l.track == track
diff --git a/changes/changelog.d/260.enhancement b/changes/changelog.d/260.enhancement
new file mode 100644
index 00000000..8d650347
--- /dev/null
+++ b/changes/changelog.d/260.enhancement
@@ -0,0 +1,2 @@
+Implemented scrobble endpoint of subsonic API, listenings are now tracked
+correctly from third party apps that use this endpoint (#260)
-- 
GitLab