from django.conf import settings from django.db import models from django.db import transaction from django.utils import timezone from rest_framework import exceptions from funkwhale_api.common import fields class Playlist(models.Model): name = models.CharField(max_length=50) user = models.ForeignKey( 'users.User', related_name="playlists", on_delete=models.CASCADE) creation_date = models.DateTimeField(default=timezone.now) modification_date = models.DateTimeField( auto_now=True) privacy_level = fields.get_privacy_field() def __str__(self): return self.name @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 if index > total: raise exceptions.ValidationError('Index is not continuous') if index < 0: raise exceptions.ValidationError('Index must be zero or positive') if move: # we remove the index temporarily, to avoid integrity errors plt.index = None plt.save(update_fields=['index']) 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) 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) to_update.update(index=models.F('index') + 1) else: to_update = existing.filter(index__gte=index) to_update.update(index=models.F('index') + 1) plt.index = index plt.save(update_fields=['index']) self.save(update_fields=['modification_date']) return index @transaction.atomic def remove(self, index): existing = self.playlist_tracks.select_for_update() self.save(update_fields=['modification_date']) to_update = existing.filter(index__gt=index) 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() if existing.count() + len(tracks) > settings.PLAYLISTS_MAX_TRACKS: raise exceptions.ValidationError( 'Playlist would reach the maximum of {} tracks'.format( settings.PLAYLISTS_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): return (self.select_related() .select_related('track__album__artist') .prefetch_related( 'track__tags', 'track__files', 'track__artist__albums__tracks__tags')) class PlaylistTrack(models.Model): track = models.ForeignKey( 'music.Track', related_name='playlist_tracks', on_delete=models.CASCADE) index = models.PositiveIntegerField(null=True, blank=True) playlist = models.ForeignKey( Playlist, related_name='playlist_tracks', on_delete=models.CASCADE) creation_date = models.DateTimeField(default=timezone.now) objects = PlaylistTrackQuerySet.as_manager() class Meta: ordering = ('-playlist', 'index') unique_together = ('playlist', 'index') def delete(self, *args, **kwargs): playlist = self.playlist index = self.index 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