From 6f70c38ecfb359e6ca03a7217f728fd9dea22701 Mon Sep 17 00:00:00 2001 From: Alexander-D-Karpov Date: Wed, 17 Jan 2024 15:43:20 +0300 Subject: [PATCH] added anon user listen history --- akarpov/music/api/serializers.py | 21 +++++++ akarpov/music/api/urls.py | 2 + akarpov/music/api/views.py | 26 +++++++- akarpov/music/models.py | 25 ++++++++ akarpov/music/tasks.py | 104 ++++++++++++++++++++----------- 5 files changed, 140 insertions(+), 38 deletions(-) diff --git a/akarpov/music/api/serializers.py b/akarpov/music/api/serializers.py index b534f24..9186f20 100644 --- a/akarpov/music/api/serializers.py +++ b/akarpov/music/api/serializers.py @@ -5,6 +5,7 @@ from akarpov.common.api.serializers import SetUserModelSerializer from akarpov.music.models import ( Album, + AnonMusicUser, Author, Playlist, PlaylistSong, @@ -198,6 +199,26 @@ def create(self, validated_data): return playlist_song +class ListenSongSerializer(serializers.Serializer): + song = serializers.SlugField() + user_id = serializers.CharField(required=False) + + def validate(self, attrs): + if not Song.objects.filter(slug=attrs["song"]).exists(): + raise serializers.ValidationError("Song not found") + + if "user_id" in attrs and attrs["user_id"]: + if not AnonMusicUser.objects.filter(id=attrs["user_id"]).exists(): + raise serializers.ValidationError("User not found") + return attrs + + +class AnonMusicUserSerializer(serializers.ModelSerializer): + class Meta: + model = AnonMusicUser + fields = ["id"] + + class LikeDislikeSongSerializer(serializers.ModelSerializer): song = serializers.SlugField() diff --git a/akarpov/music/api/urls.py b/akarpov/music/api/urls.py index cdc8644..034da7e 100644 --- a/akarpov/music/api/urls.py +++ b/akarpov/music/api/urls.py @@ -2,6 +2,7 @@ from akarpov.music.api.views import ( AddSongToPlaylistAPIView, + CreateAnonMusicUserAPIView, DislikeSongAPIView, LikeSongAPIView, ListAlbumsAPIView, @@ -76,4 +77,5 @@ RetrieveUpdateDestroyAuthorAPIView.as_view(), name="retrieve_update_delete_author", ), + path("anon/create/", CreateAnonMusicUserAPIView.as_view(), name="create-anon"), ] diff --git a/akarpov/music/api/views.py b/akarpov/music/api/views.py index a79a9d1..b24e7db 100644 --- a/akarpov/music/api/views.py +++ b/akarpov/music/api/views.py @@ -6,12 +6,14 @@ from akarpov.common.api.permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly from akarpov.music.api.serializers import ( AddSongToPlaylistSerializer, + AnonMusicUserSerializer, FullAlbumSerializer, FullAuthorSerializer, FullPlaylistSerializer, LikeDislikeSongSerializer, ListAlbumSerializer, ListAuthorSerializer, + ListenSongSerializer, ListPlaylistSerializer, ListSongSerializer, PlaylistSerializer, @@ -314,7 +316,7 @@ class RetrieveUpdateDestroyAuthorAPIView( class ListenSongAPIView(generics.GenericAPIView): - serializer_class = LikeDislikeSongSerializer + serializer_class = ListenSongSerializer permission_classes = [permissions.AllowAny] def get_queryset(self): @@ -331,12 +333,25 @@ def post(self, request, *args, **kwargs): return Response(status=404) if self.request.user.is_authenticated: listen_to_song.apply_async( - kwargs={"song_id": song.id, "user_id": self.request.user.id}, + kwargs={ + "song_id": song.id, + "user_id": self.request.user.id, + "anon": False, + }, + countdown=2, + ) + elif "user_id" in data: + listen_to_song.apply_async( + kwargs={ + "song_id": song.id, + "user_id": data["user_id"], + "anon": True, + }, countdown=2, ) else: listen_to_song.apply_async( - kwargs={"song_id": song.id}, + kwargs={"song_id": song.id, "user_id": None, "anon": True}, countdown=2, ) return Response(status=201) @@ -353,3 +368,8 @@ def get_queryset(self): .filter(user=self.request.user) .values_list("song_id", flat=True) ) + + +class CreateAnonMusicUserAPIView(generics.CreateAPIView): + serializer_class = AnonMusicUserSerializer + permission_classes = [permissions.AllowAny] diff --git a/akarpov/music/models.py b/akarpov/music/models.py index c7aaa5c..c87aed4 100644 --- a/akarpov/music/models.py +++ b/akarpov/music/models.py @@ -1,3 +1,5 @@ +import uuid + from django.contrib.postgres.fields import ArrayField from django.db import models from django.urls import reverse @@ -154,6 +156,29 @@ class Meta: ordering = ["-created"] +class AnonMusicUser(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return f"AnonMusicUser {self.id}" + + +class AnonMusicUserHistory(models.Model): + user = models.ForeignKey( + "music.AnonMusicUser", related_name="songs_listened", on_delete=models.CASCADE + ) + song = models.ForeignKey( + "Song", related_name="anon_listeners", on_delete=models.CASCADE + ) + created = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-created"] + + def __str__(self): + return f"{self.user} - {self.song}" + + class UserListenHistory(models.Model): user = models.ForeignKey( "users.User", related_name="songs_listened", on_delete=models.CASCADE diff --git a/akarpov/music/tasks.py b/akarpov/music/tasks.py index 1d26655..f3d82a5 100644 --- a/akarpov/music/tasks.py +++ b/akarpov/music/tasks.py @@ -1,3 +1,5 @@ +from datetime import timedelta + import pylast import structlog from asgiref.sync import async_to_sync @@ -9,7 +11,14 @@ from pytube import Channel, Playlist from akarpov.music.api.serializers import SongSerializer -from akarpov.music.models import RadioSong, Song, UserListenHistory, UserMusicProfile +from akarpov.music.models import ( + AnonMusicUser, + AnonMusicUserHistory, + RadioSong, + Song, + UserListenHistory, + UserMusicProfile, +) from akarpov.music.services import yandex, youtube from akarpov.music.services.file import load_dir, load_file from akarpov.utils.celery import get_scheduled_tasks_name @@ -96,7 +105,7 @@ def start_next_song(previous_ids: list): @shared_task -def listen_to_song(song_id, user_id=None): +def listen_to_song(song_id, user_id=None, anon=True): # protection from multiple listen, # check that last listen by user was more than the length of the song # and last listened song is not the same @@ -104,38 +113,63 @@ def listen_to_song(song_id, user_id=None): s.played += 1 s.save(update_fields=["played"]) if user_id: - try: - last_listen = UserListenHistory.objects.filter(user_id=user_id).latest("id") - except UserListenHistory.DoesNotExist: - last_listen = None - if ( - last_listen - and last_listen.song_id == song_id - or last_listen - and last_listen.created + s.length > now() - ): - return - UserListenHistory.objects.create( - user_id=user_id, - song_id=song_id, - ) - try: - user_profile = UserMusicProfile.objects.get(user_id=user_id) - lastfm_token = user_profile.lastfm_token - - # Initialize Last.fm network with the user's session key - network = pylast.LastFMNetwork( - api_key=settings.LAST_FM_API_KEY, - api_secret=settings.LAST_FM_SECRET, - session_key=lastfm_token, + if anon: + try: + anon_user = AnonMusicUser.objects.get(id=user_id) + except AnonMusicUser.DoesNotExist: + anon_user = AnonMusicUser.objects.create(id=user_id) + try: + last_listen = AnonMusicUserHistory.objects.filter( + user_id=user_id + ).last() + except AnonMusicUserHistory.DoesNotExist: + last_listen = None + if ( + last_listen + and last_listen.song_id == song_id + or last_listen + and last_listen.created + timedelta(seconds=s.length) > now() + ): + return + AnonMusicUserHistory.objects.create( + user=anon_user, + song_id=song_id, ) - song = Song.objects.get(id=song_id) - artist_name = song.artists_names - track_name = song.name - timestamp = int(timezone.now().timestamp()) - network.scrobble(artist=artist_name, title=track_name, timestamp=timestamp) - except UserMusicProfile.DoesNotExist: - pass - except Exception as e: - logger.error(f"Last.fm scrobble error: {e}") + else: + try: + last_listen = UserListenHistory.objects.filter(user_id=user_id).last() + except UserListenHistory.DoesNotExist: + last_listen = None + if ( + last_listen + and last_listen.song_id == song_id + or last_listen + and last_listen.created + timedelta(seconds=s.length) > now() + ): + return + UserListenHistory.objects.create( + user_id=user_id, + song_id=song_id, + ) + try: + user_profile = UserMusicProfile.objects.get(user_id=user_id) + lastfm_token = user_profile.lastfm_token + + # Initialize Last.fm network with the user's session key + network = pylast.LastFMNetwork( + api_key=settings.LAST_FM_API_KEY, + api_secret=settings.LAST_FM_SECRET, + session_key=lastfm_token, + ) + song = Song.objects.get(id=song_id) + artist_name = song.artists_names + track_name = song.name + timestamp = int(timezone.now().timestamp()) + network.scrobble( + artist=artist_name, title=track_name, timestamp=timestamp + ) + except UserMusicProfile.DoesNotExist: + pass + except Exception as e: + logger.error(f"Last.fm scrobble error: {e}") return song_id