Compare commits

..

No commits in common. "9c32235926d107edc7e3d3af2e2cf100f8edd39d" and "f6467bd12a2ab8b285773971fb0d9b4c201b2971" have entirely different histories.

7 changed files with 63 additions and 297 deletions

View File

@ -5,7 +5,6 @@
from akarpov.common.api.serializers import SetUserModelSerializer from akarpov.common.api.serializers import SetUserModelSerializer
from akarpov.music.models import ( from akarpov.music.models import (
Album, Album,
AnonMusicUser,
Author, Author,
Playlist, Playlist,
PlaylistSong, PlaylistSong,
@ -199,26 +198,6 @@ def create(self, validated_data):
return playlist_song 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): class LikeDislikeSongSerializer(serializers.ModelSerializer):
song = serializers.SlugField() song = serializers.SlugField()
@ -271,10 +250,6 @@ def create(self, validated_data):
return song_user_rating return song_user_rating
class ListSongSlugsSerializer(serializers.Serializer):
slugs = serializers.ListField(child=serializers.SlugField())
class ListPlaylistSerializer(serializers.ModelSerializer): class ListPlaylistSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Playlist model = Playlist

View File

@ -2,7 +2,6 @@
from akarpov.music.api.views import ( from akarpov.music.api.views import (
AddSongToPlaylistAPIView, AddSongToPlaylistAPIView,
CreateAnonMusicUserAPIView,
DislikeSongAPIView, DislikeSongAPIView,
LikeSongAPIView, LikeSongAPIView,
ListAlbumsAPIView, ListAlbumsAPIView,
@ -14,7 +13,6 @@
ListLikedSongsAPIView, ListLikedSongsAPIView,
ListPublicPlaylistAPIView, ListPublicPlaylistAPIView,
ListSongPlaylistsAPIView, ListSongPlaylistsAPIView,
ListSongSlugsAPIView,
ListUserListenedSongsAPIView, ListUserListenedSongsAPIView,
RemoveSongFromPlaylistAPIView, RemoveSongFromPlaylistAPIView,
RetrieveUpdateDestroyAlbumAPIView, RetrieveUpdateDestroyAlbumAPIView,
@ -42,7 +40,6 @@
name="retrieve_update_delete_playlist", name="retrieve_update_delete_playlist",
), ),
path("song/", ListCreateSongAPIView.as_view(), name="list_create_song"), path("song/", ListCreateSongAPIView.as_view(), name="list_create_song"),
path("song/slugs/", ListSongSlugsAPIView.as_view(), name="list_songs_slugs"),
path( path(
"song/<str:slug>", "song/<str:slug>",
RetrieveUpdateDestroySongAPIView.as_view(), RetrieveUpdateDestroySongAPIView.as_view(),
@ -79,5 +76,4 @@
RetrieveUpdateDestroyAuthorAPIView.as_view(), RetrieveUpdateDestroyAuthorAPIView.as_view(),
name="retrieve_update_delete_author", name="retrieve_update_delete_author",
), ),
path("anon/create/", CreateAnonMusicUserAPIView.as_view(), name="create-anon"),
] ]

View File

@ -6,17 +6,14 @@
from akarpov.common.api.permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly from akarpov.common.api.permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly
from akarpov.music.api.serializers import ( from akarpov.music.api.serializers import (
AddSongToPlaylistSerializer, AddSongToPlaylistSerializer,
AnonMusicUserSerializer,
FullAlbumSerializer, FullAlbumSerializer,
FullAuthorSerializer, FullAuthorSerializer,
FullPlaylistSerializer, FullPlaylistSerializer,
LikeDislikeSongSerializer, LikeDislikeSongSerializer,
ListAlbumSerializer, ListAlbumSerializer,
ListAuthorSerializer, ListAuthorSerializer,
ListenSongSerializer,
ListPlaylistSerializer, ListPlaylistSerializer,
ListSongSerializer, ListSongSerializer,
ListSongSlugsSerializer,
PlaylistSerializer, PlaylistSerializer,
SongSerializer, SongSerializer,
) )
@ -81,7 +78,11 @@ def get_queryset(self):
return qs.select_related("creator") return qs.select_related("creator")
class ListBaseSongAPIView(generics.ListAPIView): class ListCreateSongAPIView(LikedSongsContextMixin, generics.ListCreateAPIView):
serializer_class = ListSongSerializer
permission_classes = [IsAdminOrReadOnly]
pagination_class = StandardResultsSetPagination
def get_queryset(self): def get_queryset(self):
search = self.request.query_params.get("search", None) search = self.request.query_params.get("search", None)
if search: if search:
@ -113,14 +114,6 @@ def get_queryset(self):
) )
return qs return qs
class ListCreateSongAPIView(
LikedSongsContextMixin, generics.ListCreateAPIView, ListBaseSongAPIView
):
serializer_class = ListSongSerializer
permission_classes = [IsAdminOrReadOnly]
pagination_class = StandardResultsSetPagination
@extend_schema( @extend_schema(
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
@ -173,67 +166,6 @@ def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs) return self.list(request, *args, **kwargs)
class ListSongSlugsAPIView(ListBaseSongAPIView):
serializer_class = ListSongSlugsSerializer
permission_classes = [permissions.AllowAny]
@extend_schema(
parameters=[
OpenApiParameter(
name="search",
description="Search query",
required=False,
type=str,
),
OpenApiParameter(
name="sort",
description="Sorting algorithm",
required=False,
type=str,
examples=[
OpenApiExample(
"Default",
description="by date added",
value=None,
),
OpenApiExample(
"played",
description="by total times played",
value="played",
),
OpenApiExample(
"likes",
description="by total likes",
value="likes",
),
OpenApiExample(
"likes reversed",
description="by total likes",
value="-likes",
),
OpenApiExample(
"length",
description="by track length",
value="length",
),
OpenApiExample(
"uploaded",
description="by date uploaded",
value="uploaded",
),
],
),
]
)
def get(self, request, *args, **kwargs):
songs = self.get_queryset()
return Response(
data={
"songs": songs.values_list("slug", flat=True),
}
)
class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView): class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView):
lookup_field = "slug" lookup_field = "slug"
lookup_url_kwarg = "slug" lookup_url_kwarg = "slug"
@ -382,7 +314,7 @@ class RetrieveUpdateDestroyAuthorAPIView(
class ListenSongAPIView(generics.GenericAPIView): class ListenSongAPIView(generics.GenericAPIView):
serializer_class = ListenSongSerializer serializer_class = LikeDislikeSongSerializer
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
def get_queryset(self): def get_queryset(self):
@ -399,25 +331,12 @@ def post(self, request, *args, **kwargs):
return Response(status=404) return Response(status=404)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
listen_to_song.apply_async( listen_to_song.apply_async(
kwargs={ kwargs={"song_id": song.id, "user_id": self.request.user.id},
"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, countdown=2,
) )
else: else:
listen_to_song.apply_async( listen_to_song.apply_async(
kwargs={"song_id": song.id, "user_id": None, "anon": True}, kwargs={"song_id": song.id},
countdown=2, countdown=2,
) )
return Response(status=201) return Response(status=201)
@ -434,8 +353,3 @@ def get_queryset(self):
.filter(user=self.request.user) .filter(user=self.request.user)
.values_list("song_id", flat=True) .values_list("song_id", flat=True)
) )
class CreateAnonMusicUserAPIView(generics.CreateAPIView):
serializer_class = AnonMusicUserSerializer
permission_classes = [permissions.AllowAny]

View File

@ -1,79 +0,0 @@
# Generated by Django 4.2.8 on 2024-01-17 13:13
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
dependencies = [
("music", "0015_usermusicprofile"),
]
operations = [
migrations.CreateModel(
name="AnonMusicUser",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
],
),
migrations.AddField(
model_name="song",
name="created",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="song",
name="volume",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.IntegerField(), null=True, size=None
),
),
migrations.CreateModel(
name="AnonMusicUserHistory",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
(
"song",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="anon_listeners",
to="music.song",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="songs_listened",
to="music.anonmusicuser",
),
),
],
options={
"ordering": ["-created"],
},
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 4.2.8 on 2024-01-15 17:28
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("music", "0015_usermusicprofile"),
]
operations = [
migrations.AddField(
model_name="song",
name="volume",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.IntegerField(), null=True, size=None
),
),
]

View File

@ -1,5 +1,3 @@
import uuid
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
@ -48,7 +46,6 @@ class Song(BaseImageModel, ShortLinkModel):
creator = models.ForeignKey( creator = models.ForeignKey(
"users.User", related_name="songs", on_delete=models.SET_NULL, null=True "users.User", related_name="songs", on_delete=models.SET_NULL, null=True
) )
created = models.DateTimeField(auto_now_add=True)
meta = models.JSONField(blank=True, null=True) meta = models.JSONField(blank=True, null=True)
likes = models.IntegerField(default=0) likes = models.IntegerField(default=0)
volume = ArrayField(models.IntegerField(), null=True) volume = ArrayField(models.IntegerField(), null=True)
@ -157,29 +154,6 @@ class Meta:
ordering = ["-created"] 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): class UserListenHistory(models.Model):
user = models.ForeignKey( user = models.ForeignKey(
"users.User", related_name="songs_listened", on_delete=models.CASCADE "users.User", related_name="songs_listened", on_delete=models.CASCADE

View File

@ -1,5 +1,3 @@
from datetime import timedelta
import pylast import pylast
import structlog import structlog
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
@ -11,14 +9,7 @@
from pytube import Channel, Playlist from pytube import Channel, Playlist
from akarpov.music.api.serializers import SongSerializer from akarpov.music.api.serializers import SongSerializer
from akarpov.music.models import ( from akarpov.music.models import RadioSong, Song, UserListenHistory, UserMusicProfile
AnonMusicUser,
AnonMusicUserHistory,
RadioSong,
Song,
UserListenHistory,
UserMusicProfile,
)
from akarpov.music.services import yandex, youtube from akarpov.music.services import yandex, youtube
from akarpov.music.services.file import load_dir, load_file from akarpov.music.services.file import load_dir, load_file
from akarpov.utils.celery import get_scheduled_tasks_name from akarpov.utils.celery import get_scheduled_tasks_name
@ -105,7 +96,7 @@ def start_next_song(previous_ids: list):
@shared_task @shared_task
def listen_to_song(song_id, user_id=None, anon=True): def listen_to_song(song_id, user_id=None):
# protection from multiple listen, # protection from multiple listen,
# check that last listen by user was more than the length of the song # check that last listen by user was more than the length of the song
# and last listened song is not the same # and last listened song is not the same
@ -113,63 +104,38 @@ def listen_to_song(song_id, user_id=None, anon=True):
s.played += 1 s.played += 1
s.save(update_fields=["played"]) s.save(update_fields=["played"])
if user_id: if user_id:
if anon: try:
try: last_listen = UserListenHistory.objects.filter(user_id=user_id).latest("id")
anon_user = AnonMusicUser.objects.get(id=user_id) except UserListenHistory.DoesNotExist:
except AnonMusicUser.DoesNotExist: last_listen = None
anon_user = AnonMusicUser.objects.create(id=user_id) if (
try: last_listen
last_listen = AnonMusicUserHistory.objects.filter( and last_listen.song_id == song_id
user_id=user_id or last_listen
).last() and last_listen.created + s.length > now()
except AnonMusicUserHistory.DoesNotExist: ):
last_listen = None return
if ( UserListenHistory.objects.create(
last_listen user_id=user_id,
and last_listen.song_id == song_id song_id=song_id,
or last_listen )
and last_listen.created + timedelta(seconds=s.length) > now() try:
): user_profile = UserMusicProfile.objects.get(user_id=user_id)
return lastfm_token = user_profile.lastfm_token
AnonMusicUserHistory.objects.create(
user=anon_user,
song_id=song_id,
)
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 # Initialize Last.fm network with the user's session key
network = pylast.LastFMNetwork( network = pylast.LastFMNetwork(
api_key=settings.LAST_FM_API_KEY, api_key=settings.LAST_FM_API_KEY,
api_secret=settings.LAST_FM_SECRET, api_secret=settings.LAST_FM_SECRET,
session_key=lastfm_token, session_key=lastfm_token,
) )
song = Song.objects.get(id=song_id) song = Song.objects.get(id=song_id)
artist_name = song.artists_names artist_name = song.artists_names
track_name = song.name track_name = song.name
timestamp = int(timezone.now().timestamp()) timestamp = int(timezone.now().timestamp())
network.scrobble( network.scrobble(artist=artist_name, title=track_name, timestamp=timestamp)
artist=artist_name, title=track_name, timestamp=timestamp except UserMusicProfile.DoesNotExist:
) pass
except UserMusicProfile.DoesNotExist: except Exception as e:
pass logger.error(f"Last.fm scrobble error: {e}")
except Exception as e:
logger.error(f"Last.fm scrobble error: {e}")
return song_id return song_id