Skip to content
Snippets Groups Projects
models.py 7 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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