Merge branch 'markov-radio' into 'develop'

[Experimental] Added a new "Similar" radio based on users history (suggested by @gordon)

See merge request funkwhale/funkwhale!578
merge-requests/622/head
Eliot Berriot 2019-01-30 16:57:36 +01:00
commit e6a5f69f5b
4 zmienionych plików z 95 dodań i 4 usunięć

Wyświetl plik

@ -1,7 +1,7 @@
import random
from django.core.exceptions import ValidationError
from django.db.models import Count
from django.db import connection
from rest_framework import serializers
from taggit.models import Tag
@ -43,8 +43,7 @@ class SessionRadio(SimpleRadio):
return self.session
def get_queryset(self, **kwargs):
qs = Track.objects.annotate(uploads_count=Count("uploads"))
return qs.filter(uploads_count__gt=0)
return Track.objects.all()
def get_queryset_kwargs(self):
return {}
@ -56,6 +55,10 @@ class SessionRadio(SimpleRadio):
queryset = self.filter_from_session(queryset)
if kwargs.pop("filter_playable", True):
queryset = queryset.playable_by(self.session.user.actor)
queryset = self.filter_queryset(queryset)
return queryset
def filter_queryset(self, queryset):
return queryset
def filter_from_session(self, queryset):
@ -153,6 +156,74 @@ class TagRadio(RelatedObjectRadio):
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")
class ArtistRadio(RelatedObjectRadio):
model = Artist

Wyświetl plik

@ -237,3 +237,20 @@ def test_can_start_less_listened_radio(factories):
for i in range(5):
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

Wyświetl plik

@ -0,0 +1 @@
[Experimental] Added a new "Similar" radio based on users history (suggested by @gordon)

Wyświetl plik

@ -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="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 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>
</span>
@ -62,7 +63,8 @@ export default {
return {
playNow: this.$gettext('Play now'),
addToQueue: this.$gettext('Add to current queue'),
playNext: this.$gettext('Play next')
playNext: this.$gettext('Play next'),
startRadio: this.$gettext('Play similar songs')
}
},
title () {