2018-06-10 08:55:16 +00:00
|
|
|
from django.db import models, transaction
|
2017-06-23 21:00:42 +00:00
|
|
|
from django.utils import timezone
|
2018-03-20 18:56:42 +00:00
|
|
|
from rest_framework import exceptions
|
|
|
|
|
2018-06-10 08:55:16 +00:00
|
|
|
from funkwhale_api.common import fields, preferences
|
2018-07-17 11:09:13 +00:00
|
|
|
from funkwhale_api.music import models as music_models
|
2018-03-16 22:30:37 +00:00
|
|
|
|
2017-06-23 21:00:42 +00:00
|
|
|
|
2018-05-08 21:06:29 +00:00
|
|
|
class PlaylistQuerySet(models.QuerySet):
|
|
|
|
def with_tracks_count(self):
|
2018-06-09 13:36:16 +00:00
|
|
|
return self.annotate(_tracks_count=models.Count("playlist_tracks"))
|
2018-05-08 21:06:29 +00:00
|
|
|
|
2018-07-17 11:09:13 +00:00
|
|
|
def with_duration(self):
|
|
|
|
return self.annotate(
|
2018-09-22 12:29:30 +00:00
|
|
|
duration=models.Sum("playlist_tracks__track__uploads__duration")
|
2018-07-17 11:09:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def with_covers(self):
|
|
|
|
album_prefetch = models.Prefetch(
|
2019-02-14 09:49:06 +00:00
|
|
|
"album", queryset=music_models.Album.objects.only("cover", "artist_id")
|
2018-07-17 11:09:13 +00:00
|
|
|
)
|
|
|
|
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)
|
|
|
|
|
2018-10-26 12:18:50 +00:00
|
|
|
def with_playable_plts(self, actor):
|
|
|
|
return self.prefetch_related(
|
|
|
|
models.Prefetch(
|
|
|
|
"playlist_tracks",
|
|
|
|
queryset=PlaylistTrack.objects.playable_by(actor),
|
|
|
|
to_attr="playable_plts",
|
|
|
|
)
|
2018-09-28 14:45:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def playable_by(self, actor, include=True):
|
|
|
|
plts = PlaylistTrack.objects.playable_by(actor, include)
|
|
|
|
if include:
|
2018-10-21 17:58:05 +00:00
|
|
|
return self.filter(playlist_tracks__in=plts).distinct()
|
2018-09-28 14:45:28 +00:00
|
|
|
else:
|
2018-10-21 17:58:05 +00:00
|
|
|
return self.exclude(playlist_tracks__in=plts).distinct()
|
2018-09-28 14:45:28 +00:00
|
|
|
|
2018-05-08 21:06:29 +00:00
|
|
|
|
2017-06-23 21:00:42 +00:00
|
|
|
class Playlist(models.Model):
|
|
|
|
name = models.CharField(max_length=50)
|
2017-12-15 23:36:06 +00:00
|
|
|
user = models.ForeignKey(
|
2018-06-09 13:36:16 +00:00
|
|
|
"users.User", related_name="playlists", on_delete=models.CASCADE
|
|
|
|
)
|
2017-06-23 21:00:42 +00:00
|
|
|
creation_date = models.DateTimeField(default=timezone.now)
|
2018-06-09 13:36:16 +00:00
|
|
|
modification_date = models.DateTimeField(auto_now=True)
|
2018-03-16 22:30:37 +00:00
|
|
|
privacy_level = fields.get_privacy_field()
|
2017-06-23 21:00:42 +00:00
|
|
|
|
2018-05-08 21:06:29 +00:00
|
|
|
objects = PlaylistQuerySet.as_manager()
|
|
|
|
|
2017-06-23 21:00:42 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
2018-03-19 13:11:09 +00:00
|
|
|
@transaction.atomic
|
2019-04-24 09:31:46 +00:00
|
|
|
def insert(self, plt, index=None, allow_duplicates=True):
|
2018-03-19 13:11:09 +00:00
|
|
|
"""
|
|
|
|
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
|
2017-06-23 21:00:42 +00:00
|
|
|
|
2018-03-19 13:11:09 +00:00
|
|
|
existing = self.playlist_tracks.select_for_update()
|
|
|
|
if move:
|
|
|
|
existing = existing.exclude(pk=plt.pk)
|
|
|
|
total = existing.filter(index__isnull=False).count()
|
2017-06-23 21:00:42 +00:00
|
|
|
|
2018-03-19 13:11:09 +00:00
|
|
|
if index is None:
|
|
|
|
# we simply increment the last track index by 1
|
|
|
|
index = total
|
2017-06-23 21:00:42 +00:00
|
|
|
|
2018-03-19 13:11:09 +00:00
|
|
|
if index > total:
|
2018-06-09 13:36:16 +00:00
|
|
|
raise exceptions.ValidationError("Index is not continuous")
|
2018-03-19 13:11:09 +00:00
|
|
|
|
|
|
|
if index < 0:
|
2018-06-09 13:36:16 +00:00
|
|
|
raise exceptions.ValidationError("Index must be zero or positive")
|
2018-03-19 13:11:09 +00:00
|
|
|
|
2019-04-24 09:31:46 +00:00
|
|
|
if not allow_duplicates:
|
|
|
|
existing_without_current_plt = existing.exclude(pk=plt.pk)
|
|
|
|
self._check_duplicate_add(existing_without_current_plt, [plt.track])
|
|
|
|
|
2018-03-19 13:11:09 +00:00
|
|
|
if move:
|
|
|
|
# we remove the index temporarily, to avoid integrity errors
|
|
|
|
plt.index = None
|
2018-06-09 13:36:16 +00:00
|
|
|
plt.save(update_fields=["index"])
|
2018-03-19 13:11:09 +00:00
|
|
|
if index > old_index:
|
|
|
|
# new index is higher than current, we decrement previous tracks
|
2018-06-09 13:36:16 +00:00
|
|
|
to_update = existing.filter(index__gt=old_index, index__lte=index)
|
|
|
|
to_update.update(index=models.F("index") - 1)
|
2018-03-19 13:11:09 +00:00
|
|
|
if index < old_index:
|
|
|
|
# new index is lower than current, we increment next tracks
|
2018-03-20 18:56:42 +00:00
|
|
|
to_update = existing.filter(index__lt=old_index, index__gte=index)
|
2018-06-09 13:36:16 +00:00
|
|
|
to_update.update(index=models.F("index") + 1)
|
2018-03-19 13:11:09 +00:00
|
|
|
else:
|
|
|
|
to_update = existing.filter(index__gte=index)
|
2018-06-09 13:36:16 +00:00
|
|
|
to_update.update(index=models.F("index") + 1)
|
2018-03-19 13:11:09 +00:00
|
|
|
|
|
|
|
plt.index = index
|
2018-06-09 13:36:16 +00:00
|
|
|
plt.save(update_fields=["index"])
|
|
|
|
self.save(update_fields=["modification_date"])
|
2018-03-19 13:11:09 +00:00
|
|
|
return index
|
|
|
|
|
2018-03-19 18:07:45 +00:00
|
|
|
@transaction.atomic
|
2018-03-19 14:28:33 +00:00
|
|
|
def remove(self, index):
|
|
|
|
existing = self.playlist_tracks.select_for_update()
|
2018-06-09 13:36:16 +00:00
|
|
|
self.save(update_fields=["modification_date"])
|
2018-03-19 14:28:33 +00:00
|
|
|
to_update = existing.filter(index__gt=index)
|
2018-06-09 13:36:16 +00:00
|
|
|
return to_update.update(index=models.F("index") - 1)
|
2018-03-19 14:28:33 +00:00
|
|
|
|
2018-03-20 18:56:42 +00:00
|
|
|
@transaction.atomic
|
2019-04-24 09:31:46 +00:00
|
|
|
def insert_many(self, tracks, allow_duplicates=True):
|
2018-03-20 18:56:42 +00:00
|
|
|
existing = self.playlist_tracks.select_for_update()
|
|
|
|
now = timezone.now()
|
|
|
|
total = existing.filter(index__isnull=False).count()
|
2018-06-09 13:36:16 +00:00
|
|
|
max_tracks = preferences.get("playlists__max_tracks")
|
2018-04-28 03:30:23 +00:00
|
|
|
if existing.count() + len(tracks) > max_tracks:
|
2018-03-20 18:56:42 +00:00
|
|
|
raise exceptions.ValidationError(
|
2018-06-09 13:36:16 +00:00
|
|
|
"Playlist would reach the maximum of {} tracks".format(max_tracks)
|
|
|
|
)
|
2019-04-24 09:31:46 +00:00
|
|
|
|
|
|
|
if not allow_duplicates:
|
|
|
|
self._check_duplicate_add(existing, tracks)
|
|
|
|
|
2018-06-09 13:36:16 +00:00
|
|
|
self.save(update_fields=["modification_date"])
|
2018-03-20 18:56:42 +00:00
|
|
|
start = total
|
|
|
|
plts = [
|
|
|
|
PlaylistTrack(
|
2018-06-09 13:36:16 +00:00
|
|
|
creation_date=now, playlist=self, track=track, index=start + i
|
|
|
|
)
|
2018-03-20 18:56:42 +00:00
|
|
|
for i, track in enumerate(tracks)
|
|
|
|
]
|
|
|
|
return PlaylistTrack.objects.bulk_create(plts)
|
|
|
|
|
2019-04-24 09:31:46 +00:00
|
|
|
def _check_duplicate_add(self, existing_playlist_tracks, tracks_to_add):
|
|
|
|
track_ids = [t.pk for t in tracks_to_add]
|
|
|
|
|
|
|
|
duplicates = existing_playlist_tracks.filter(
|
|
|
|
track__pk__in=track_ids
|
|
|
|
).values_list("track__pk", flat=True)
|
|
|
|
if duplicates:
|
|
|
|
duplicate_tracks = [t for t in tracks_to_add if t.pk in duplicates]
|
|
|
|
raise exceptions.ValidationError(
|
|
|
|
{
|
|
|
|
"non_field_errors": [
|
|
|
|
{
|
|
|
|
"tracks": duplicate_tracks,
|
|
|
|
"playlist_name": self.name,
|
|
|
|
"code": "tracks_already_exist_in_playlist",
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2018-06-09 13:36:16 +00:00
|
|
|
|
2018-03-20 21:44:28 +00:00
|
|
|
class PlaylistTrackQuerySet(models.QuerySet):
|
2018-09-28 18:46:18 +00:00
|
|
|
def for_nested_serialization(self, actor=None):
|
2018-10-26 12:18:50 +00:00
|
|
|
tracks = music_models.Track.objects.with_playable_uploads(actor)
|
2018-09-28 18:46:18 +00:00
|
|
|
tracks = tracks.select_related("artist", "album__artist")
|
2018-09-28 18:50:28 +00:00
|
|
|
return self.prefetch_related(
|
|
|
|
models.Prefetch("track", queryset=tracks, to_attr="_prefetched_track")
|
|
|
|
)
|
2018-03-20 21:44:28 +00:00
|
|
|
|
2018-09-28 14:45:28 +00:00
|
|
|
def annotate_playable_by_actor(self, actor):
|
|
|
|
tracks = (
|
2018-10-26 12:18:50 +00:00
|
|
|
music_models.Upload.objects.playable_by(actor)
|
|
|
|
.filter(track__pk=models.OuterRef("track"))
|
2018-09-28 14:45:28 +00:00
|
|
|
.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:
|
2018-10-21 17:58:05 +00:00
|
|
|
return self.filter(track__pk__in=tracks).distinct()
|
2018-09-28 14:45:28 +00:00
|
|
|
else:
|
2018-10-21 17:58:05 +00:00
|
|
|
return self.exclude(track__pk__in=tracks).distinct()
|
2018-09-28 14:45:28 +00:00
|
|
|
|
2018-03-19 13:11:09 +00:00
|
|
|
|
|
|
|
class PlaylistTrack(models.Model):
|
2017-12-15 23:36:06 +00:00
|
|
|
track = models.ForeignKey(
|
2018-06-09 13:36:16 +00:00
|
|
|
"music.Track", related_name="playlist_tracks", on_delete=models.CASCADE
|
|
|
|
)
|
2018-03-19 14:28:33 +00:00
|
|
|
index = models.PositiveIntegerField(null=True, blank=True)
|
2017-12-15 23:36:06 +00:00
|
|
|
playlist = models.ForeignKey(
|
2018-06-09 13:36:16 +00:00
|
|
|
Playlist, related_name="playlist_tracks", on_delete=models.CASCADE
|
|
|
|
)
|
2018-03-19 13:11:09 +00:00
|
|
|
creation_date = models.DateTimeField(default=timezone.now)
|
2017-06-23 21:00:42 +00:00
|
|
|
|
2018-03-20 21:44:28 +00:00
|
|
|
objects = PlaylistTrackQuerySet.as_manager()
|
|
|
|
|
2017-06-23 21:00:42 +00:00
|
|
|
class Meta:
|
2018-06-09 13:36:16 +00:00
|
|
|
ordering = ("-playlist", "index")
|
|
|
|
unique_together = ("playlist", "index")
|
2018-03-19 14:28:33 +00:00
|
|
|
|
|
|
|
def delete(self, *args, **kwargs):
|
|
|
|
playlist = self.playlist
|
|
|
|
index = self.index
|
2018-06-09 13:36:16 +00:00
|
|
|
update_indexes = kwargs.pop("update_indexes", False)
|
2018-03-19 14:28:33 +00:00
|
|
|
r = super().delete(*args, **kwargs)
|
|
|
|
if index is not None and update_indexes:
|
|
|
|
playlist.remove(index)
|
|
|
|
return r
|