Commit e6a5f69f authored by Eliot Berriot's avatar Eliot Berriot
Browse files

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
parents 602a4c3b 5ce4cc83
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(
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):
class SimilarRadio(RelatedObjectRadio):
model = Track
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
seeds = list(
.values_list("track_id", flat=True)
) + []
for seed in seeds:
return queryset.filter(pk=self.find_next_id(queryset, seed))
except NextNotFound:
return queryset.none()
def find_next_id(self, queryset, seed):
with connection.cursor() as cursor:
query = """
SELECT next, count(next) AS c
LEAD(track_id) OVER (
PARTITION by user_id order by creation_date asc
) AS next
FROM history_listening
INNER JOIN users_user ON ( = 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)
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)
# 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
[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:})" :title="labels.startRadio"><i class="feed icon"></i><translate>Start radio</translate></button>
......@@ -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 () {
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment