kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Merge branch 'markov-radio' into 'develop'
[Experimental] Added a new "Similar" radio based on users history (suggested by @gordon) See merge request funkwhale/funkwhale!578merge-requests/622/head
commit
e6a5f69f5b
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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="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 () {
|
||||
|
|
Ładowanie…
Reference in New Issue