Newer
Older
Eliot Berriot
committed
from django.utils import timezone
from rest_framework import exceptions
from funkwhale_api.common import fields, preferences
from funkwhale_api.music import models as music_models
Eliot Berriot
committed
class PlaylistQuerySet(models.QuerySet):
def with_tracks_count(self):
return self.annotate(_tracks_count=models.Count("playlist_tracks"))
def with_duration(self):
return self.annotate(
duration=models.Sum("playlist_tracks__track__uploads__duration")
)
def with_covers(self):
album_prefetch = models.Prefetch(
"album", queryset=music_models.Album.objects.only("cover")
)
track_prefetch = models.Prefetch(
"track",
queryset=music_models.Track.objects.prefetch_related(album_prefetch).only(
"id", "album_id"
),
)
plt_prefetch = models.Prefetch(
"playlist_tracks",
queryset=PlaylistTrack.objects.all()
.exclude(track__album__cover=None)
.exclude(track__album__cover="")
.order_by("index")
.only("id", "playlist_id", "track_id")
.prefetch_related(track_prefetch),
to_attr="plts_for_cover",
)
return self.prefetch_related(plt_prefetch)
def annotate_playable_by_actor(self, actor):
plts = (
PlaylistTrack.objects.playable_by(actor)
.filter(playlist=models.OuterRef("id"))
.order_by("id")
.values("id")[:1]
)
subquery = models.Subquery(plts)
return self.annotate(is_playable_by_actor=subquery)
def playable_by(self, actor, include=True):
plts = PlaylistTrack.objects.playable_by(actor, include)
if include:
return self.filter(playlist_tracks__in=plts)
else:
return self.exclude(playlist_tracks__in=plts)
Eliot Berriot
committed
class Playlist(models.Model):
name = models.CharField(max_length=50)
"users.User", related_name="playlists", on_delete=models.CASCADE
)
Eliot Berriot
committed
creation_date = models.DateTimeField(default=timezone.now)
privacy_level = fields.get_privacy_field()
Eliot Berriot
committed
objects = PlaylistQuerySet.as_manager()
Eliot Berriot
committed
def __str__(self):
return self.name
Eliot Berriot
committed
@transaction.atomic
def insert(self, plt, index=None):
"""
Given a PlaylistTrack, insert it at the correct index in the playlist,
and update other tracks index if necessary.
"""
old_index = plt.index
move = old_index is not None
if index is not None and index == old_index:
# moving at same position, just skip
return index
Eliot Berriot
committed
Eliot Berriot
committed
existing = self.playlist_tracks.select_for_update()
if move:
existing = existing.exclude(pk=plt.pk)
total = existing.filter(index__isnull=False).count()
Eliot Berriot
committed
Eliot Berriot
committed
if index is None:
# we simply increment the last track index by 1
index = total
Eliot Berriot
committed
Eliot Berriot
committed
if index > total:
raise exceptions.ValidationError("Index is not continuous")
Eliot Berriot
committed
if index < 0:
raise exceptions.ValidationError("Index must be zero or positive")
Eliot Berriot
committed
if move:
# we remove the index temporarily, to avoid integrity errors
plt.index = None
Eliot Berriot
committed
if index > old_index:
# new index is higher than current, we decrement previous tracks
to_update = existing.filter(index__gt=old_index, index__lte=index)
to_update.update(index=models.F("index") - 1)
Eliot Berriot
committed
if index < old_index:
# new index is lower than current, we increment next tracks
to_update = existing.filter(index__lt=old_index, index__gte=index)
Eliot Berriot
committed
else:
to_update = existing.filter(index__gte=index)
Eliot Berriot
committed
plt.index = index
plt.save(update_fields=["index"])
self.save(update_fields=["modification_date"])
Eliot Berriot
committed
return index
@transaction.atomic
def remove(self, index):
existing = self.playlist_tracks.select_for_update()
to_update = existing.filter(index__gt=index)
@transaction.atomic
def insert_many(self, tracks):
existing = self.playlist_tracks.select_for_update()
now = timezone.now()
total = existing.filter(index__isnull=False).count()
if existing.count() + len(tracks) > max_tracks:
raise exceptions.ValidationError(
"Playlist would reach the maximum of {} tracks".format(max_tracks)
)
self.save(update_fields=["modification_date"])
start = total
plts = [
PlaylistTrack(
creation_date=now, playlist=self, track=track, index=start + i
)
for i, track in enumerate(tracks)
]
return PlaylistTrack.objects.bulk_create(plts)
class PlaylistTrackQuerySet(models.QuerySet):
def for_nested_serialization(self, actor=None):
tracks = music_models.Track.objects.annotate_playable_by_actor(actor)
tracks = tracks.select_related("artist", "album__artist")
return self.prefetch_related(
models.Prefetch("track", queryset=tracks, to_attr="_prefetched_track")
)
def annotate_playable_by_actor(self, actor):
tracks = (
music_models.Track.objects.playable_by(actor)
.filter(pk=models.OuterRef("track"))
.order_by("id")
.values("id")[:1]
)
subquery = models.Subquery(tracks)
return self.annotate(is_playable_by_actor=subquery)
def playable_by(self, actor, include=True):
tracks = music_models.Track.objects.playable_by(actor, include)
if include:
return self.filter(track__pk__in=tracks)
else:
return self.exclude(track__pk__in=tracks)
Eliot Berriot
committed
class PlaylistTrack(models.Model):
"music.Track", related_name="playlist_tracks", on_delete=models.CASCADE
)
index = models.PositiveIntegerField(null=True, blank=True)
Playlist, related_name="playlist_tracks", on_delete=models.CASCADE
)
Eliot Berriot
committed
creation_date = models.DateTimeField(default=timezone.now)
Eliot Berriot
committed
objects = PlaylistTrackQuerySet.as_manager()
Eliot Berriot
committed
class Meta:
ordering = ("-playlist", "index")
unique_together = ("playlist", "index")
def delete(self, *args, **kwargs):
playlist = self.playlist
index = self.index
r = super().delete(*args, **kwargs)
if index is not None and update_indexes:
playlist.remove(index)
return r