[Experimental] Added a new "Similar" radio based on users history (suggested by @gordon)
This commit is contained in:
parent
602a4c3b29
commit
5ce4cc8379
|
@ -1,7 +1,7 @@
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Count
|
from django.db import connection
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
|
@ -43,8 +43,7 @@ class SessionRadio(SimpleRadio):
|
||||||
return self.session
|
return self.session
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = Track.objects.annotate(uploads_count=Count("uploads"))
|
return Track.objects.all()
|
||||||
return qs.filter(uploads_count__gt=0)
|
|
||||||
|
|
||||||
def get_queryset_kwargs(self):
|
def get_queryset_kwargs(self):
|
||||||
return {}
|
return {}
|
||||||
|
@ -56,6 +55,10 @@ class SessionRadio(SimpleRadio):
|
||||||
queryset = self.filter_from_session(queryset)
|
queryset = self.filter_from_session(queryset)
|
||||||
if kwargs.pop("filter_playable", True):
|
if kwargs.pop("filter_playable", True):
|
||||||
queryset = queryset.playable_by(self.session.user.actor)
|
queryset = queryset.playable_by(self.session.user.actor)
|
||||||
|
queryset = self.filter_queryset(queryset)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def filter_from_session(self, queryset):
|
def filter_from_session(self, queryset):
|
||||||
|
@ -153,6 +156,74 @@ class TagRadio(RelatedObjectRadio):
|
||||||
return qs.filter(tags__in=[self.session.related_object])
|
return qs.filter(tags__in=[self.session.related_object])
|
||||||
|
|
||||||
|
|
||||||
|
def weighted_choice(choices):
|
||||||
|
total = sum(w for c, w in choices)
|
||||||
|
r = random.uniform(0, total)
|
||||||
|
upto = 0
|
||||||
|
for c, w in choices:
|
||||||
|
if upto + w >= r:
|
||||||
|
return c
|
||||||
|
upto += w
|
||||||
|
assert False, "Shouldn't get here"
|
||||||
|
|
||||||
|
|
||||||
|
class NextNotFound(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register(name="similar")
|
||||||
|
class SimilarRadio(RelatedObjectRadio):
|
||||||
|
model = Track
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
seeds = list(
|
||||||
|
self.session.session_tracks.all()
|
||||||
|
.values_list("track_id", flat=True)
|
||||||
|
.order_by("-id")[:3]
|
||||||
|
) + [self.session.related_object.pk]
|
||||||
|
for seed in seeds:
|
||||||
|
try:
|
||||||
|
return queryset.filter(pk=self.find_next_id(queryset, seed))
|
||||||
|
except NextNotFound:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
|
def find_next_id(self, queryset, seed):
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
query = """
|
||||||
|
SELECT next, count(next) AS c
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
track_id,
|
||||||
|
creation_date,
|
||||||
|
LEAD(track_id) OVER (
|
||||||
|
PARTITION by user_id order by creation_date asc
|
||||||
|
) AS next
|
||||||
|
FROM history_listening
|
||||||
|
INNER JOIN users_user ON (users_user.id = user_id)
|
||||||
|
WHERE users_user.privacy_level = 'instance' OR users_user.privacy_level = 'everyone' OR user_id = %s
|
||||||
|
ORDER BY creation_date ASC
|
||||||
|
) t WHERE track_id = %s AND next != %s GROUP BY next ORDER BY c DESC;
|
||||||
|
"""
|
||||||
|
cursor.execute(query, [self.session.user_id, seed, seed])
|
||||||
|
next_candidates = list(cursor.fetchall())
|
||||||
|
|
||||||
|
if not next_candidates:
|
||||||
|
raise NextNotFound()
|
||||||
|
|
||||||
|
matching_tracks = list(
|
||||||
|
queryset.filter(pk__in=[c[0] for c in next_candidates]).values_list(
|
||||||
|
"id", flat=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
next_candidates = [n for n in next_candidates if n[0] in matching_tracks]
|
||||||
|
if not next_candidates:
|
||||||
|
raise NextNotFound()
|
||||||
|
return weighted_choice(next_candidates)
|
||||||
|
|
||||||
|
|
||||||
@registry.register(name="artist")
|
@registry.register(name="artist")
|
||||||
class ArtistRadio(RelatedObjectRadio):
|
class ArtistRadio(RelatedObjectRadio):
|
||||||
model = Artist
|
model = Artist
|
||||||
|
|
|
@ -237,3 +237,20 @@ def test_can_start_less_listened_radio(factories):
|
||||||
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
assert radio.pick(filter_playable=False) in good_tracks
|
assert radio.pick(filter_playable=False) in good_tracks
|
||||||
|
|
||||||
|
|
||||||
|
def test_similar_radio_track(factories):
|
||||||
|
user = factories["users.User"]()
|
||||||
|
seed = factories["music.Track"]()
|
||||||
|
radio = radios.SimilarRadio()
|
||||||
|
radio.start_session(user, related_object=seed)
|
||||||
|
|
||||||
|
factories["music.Track"].create_batch(5)
|
||||||
|
|
||||||
|
# one user listened to this track
|
||||||
|
l1 = factories["history.Listening"](track=seed)
|
||||||
|
|
||||||
|
expected_next = factories["music.Track"]()
|
||||||
|
factories["history.Listening"](track=expected_next, user=l1.user)
|
||||||
|
|
||||||
|
assert radio.pick(filter_playable=False) == expected_next
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[Experimental] Added a new "Similar" radio based on users history (suggested by @gordon)
|
|
@ -15,6 +15,7 @@
|
||||||
<button class="item basic" ref="add" data-ref="add" :disabled="!playable" @click.stop.prevent="add" :title="labels.addToQueue"><i class="plus icon"></i><translate>Add to queue</translate></button>
|
<button class="item basic" ref="add" data-ref="add" :disabled="!playable" @click.stop.prevent="add" :title="labels.addToQueue"><i class="plus icon"></i><translate>Add to queue</translate></button>
|
||||||
<button class="item basic" ref="addNext" data-ref="addNext" :disabled="!playable" @click.stop.prevent="addNext()" :title="labels.playNext"><i class="step forward icon"></i><translate>Play next</translate></button>
|
<button class="item basic" ref="addNext" data-ref="addNext" :disabled="!playable" @click.stop.prevent="addNext()" :title="labels.playNext"><i class="step forward icon"></i><translate>Play next</translate></button>
|
||||||
<button class="item basic" ref="playNow" data-ref="playNow" :disabled="!playable" @click.stop.prevent="addNext(true)" :title="labels.playNow"><i class="play icon"></i><translate>Play now</translate></button>
|
<button class="item basic" ref="playNow" data-ref="playNow" :disabled="!playable" @click.stop.prevent="addNext(true)" :title="labels.playNow"><i class="play icon"></i><translate>Play now</translate></button>
|
||||||
|
<button v-if="track" class="item basic" :disabled="!playable" @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" :title="labels.startRadio"><i class="feed icon"></i><translate>Start radio</translate></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@ -62,7 +63,8 @@ export default {
|
||||||
return {
|
return {
|
||||||
playNow: this.$gettext('Play now'),
|
playNow: this.$gettext('Play now'),
|
||||||
addToQueue: this.$gettext('Add to current queue'),
|
addToQueue: this.$gettext('Add to current queue'),
|
||||||
playNext: this.$gettext('Play next')
|
playNext: this.$gettext('Play next'),
|
||||||
|
startRadio: this.$gettext('Play similar songs')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title () {
|
title () {
|
||||||
|
|
Loading…
Reference in New Issue