Skip to content
Snippets Groups Projects
models.py 7 KiB
Newer Older
Eliot Berriot's avatar
Eliot Berriot committed
from django.db import models, transaction
from rest_framework import exceptions

Eliot Berriot's avatar
Eliot Berriot committed
from funkwhale_api.common import fields, preferences
from funkwhale_api.music import models as music_models
class PlaylistQuerySet(models.QuerySet):
    def with_tracks_count(self):
Eliot Berriot's avatar
Eliot Berriot committed
        return self.annotate(_tracks_count=models.Count("playlist_tracks"))
    def with_duration(self):
        return self.annotate(
Eliot Berriot's avatar
Eliot Berriot committed
            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)

class Playlist(models.Model):
    name = models.CharField(max_length=50)
    user = models.ForeignKey(
Eliot Berriot's avatar
Eliot Berriot committed
        "users.User", related_name="playlists", on_delete=models.CASCADE
    )
    creation_date = models.DateTimeField(default=timezone.now)
Eliot Berriot's avatar
Eliot Berriot committed
    modification_date = models.DateTimeField(auto_now=True)
    privacy_level = fields.get_privacy_field()
    objects = PlaylistQuerySet.as_manager()

    @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
        existing = self.playlist_tracks.select_for_update()
        if move:
            existing = existing.exclude(pk=plt.pk)
        total = existing.filter(index__isnull=False).count()
        if index is None:
            # we simply increment the last track index by 1
            index = total
Eliot Berriot's avatar
Eliot Berriot committed
            raise exceptions.ValidationError("Index is not continuous")
Eliot Berriot's avatar
Eliot Berriot committed
            raise exceptions.ValidationError("Index must be zero or positive")

        if move:
            # we remove the index temporarily, to avoid integrity errors
            plt.index = None
Eliot Berriot's avatar
Eliot Berriot committed
            plt.save(update_fields=["index"])
            if index > old_index:
                # new index is higher than current, we decrement previous tracks
Eliot Berriot's avatar
Eliot Berriot committed
                to_update = existing.filter(index__gt=old_index, index__lte=index)
                to_update.update(index=models.F("index") - 1)
            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's avatar
Eliot Berriot committed
                to_update.update(index=models.F("index") + 1)
        else:
            to_update = existing.filter(index__gte=index)
Eliot Berriot's avatar
Eliot Berriot committed
            to_update.update(index=models.F("index") + 1)
Eliot Berriot's avatar
Eliot Berriot committed
        plt.save(update_fields=["index"])
        self.save(update_fields=["modification_date"])
    def remove(self, index):
        existing = self.playlist_tracks.select_for_update()
Eliot Berriot's avatar
Eliot Berriot committed
        self.save(update_fields=["modification_date"])
        to_update = existing.filter(index__gt=index)
Eliot Berriot's avatar
Eliot Berriot committed
        return to_update.update(index=models.F("index") - 1)
    @transaction.atomic
    def insert_many(self, tracks):
        existing = self.playlist_tracks.select_for_update()
        now = timezone.now()
        total = existing.filter(index__isnull=False).count()
Eliot Berriot's avatar
Eliot Berriot committed
        max_tracks = preferences.get("playlists__max_tracks")
        if existing.count() + len(tracks) > max_tracks:
            raise exceptions.ValidationError(
Eliot Berriot's avatar
Eliot Berriot committed
                "Playlist would reach the maximum of {} tracks".format(max_tracks)
            )
        self.save(update_fields=["modification_date"])
        start = total
        plts = [
            PlaylistTrack(
Eliot Berriot's avatar
Eliot Berriot committed
                creation_date=now, playlist=self, track=track, index=start + i
            )
            for i, track in enumerate(tracks)
        ]
        return PlaylistTrack.objects.bulk_create(plts)

Eliot Berriot's avatar
Eliot Berriot committed

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")
Eliot Berriot's avatar
Eliot Berriot committed
        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)

    track = models.ForeignKey(
Eliot Berriot's avatar
Eliot Berriot committed
        "music.Track", related_name="playlist_tracks", on_delete=models.CASCADE
    )
    index = models.PositiveIntegerField(null=True, blank=True)
    playlist = models.ForeignKey(
Eliot Berriot's avatar
Eliot Berriot committed
        Playlist, related_name="playlist_tracks", on_delete=models.CASCADE
    )
    creation_date = models.DateTimeField(default=timezone.now)
    objects = PlaylistTrackQuerySet.as_manager()

Eliot Berriot's avatar
Eliot Berriot committed
        ordering = ("-playlist", "index")
        unique_together = ("playlist", "index")

    def delete(self, *args, **kwargs):
        playlist = self.playlist
        index = self.index
Eliot Berriot's avatar
Eliot Berriot committed
        update_indexes = kwargs.pop("update_indexes", False)
        r = super().delete(*args, **kwargs)
        if index is not None and update_indexes:
            playlist.remove(index)
        return r