diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py
index f63ad5d2ee92885e5ec15713bfbf82d92ac2495a..97cdbcfc692b97bc763771a903888372768d47a7 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 87c9f72700ca8dcd7e5cbfeb3fadb37edb6c7e11..dbc31ec5f22efdee1b0444759aa1d454d9aa053e 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 081b669c571cceff6d41fd973bfd8b3113a35a60..6b9ec232da9e8789fd3bdbd60147c2d706b3e1ad 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 65c2ad95f351ca2228f4d05fc835f34c4edaf1e7..52e410e52b4c516b58ecc05a45df595316748553 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 0000000000000000000000000000000000000000..8d65034731ff9aca4b37fd2f20202c846d8f59af
--- /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)