Resolve "Add "play all" button in tag search result page"

This commit is contained in:
petitminion 2022-07-15 09:07:15 +00:00 committed by Georg Krause
parent 3d825cd170
commit 0f4226e06f
11 changed files with 187 additions and 38 deletions

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.10 on 2022-01-21 11:56
import django.contrib.postgres.fields.jsonb
import django.core.serializers.json
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('radios', '0005_auto_20200803_1222'),
]
operations = [
migrations.AddField(
model_name='radiosession',
name='config',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 3.2.13 on 2022-07-15 08:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('radios', '0006_alter_radio_config'),
('radios', '0006_radiosession_config'),
]
operations = [
]

View File

@ -51,6 +51,8 @@ class RadioSession(models.Model):
related_object = GenericForeignKey(
"related_object_content_type", "related_object_id"
)
CONFIG_VERSION = 0
config = JSONField(encoder=DjangoJSONEncoder, blank=True, null=True)
def save(self, **kwargs):
self.radio.clean(self)

View File

@ -1,4 +1,5 @@
import datetime
import logging
import random
from django.core.exceptions import ValidationError
@ -15,6 +16,8 @@ from funkwhale_api.tags.models import Tag
from . import filters, models
from .registries import registry
logger = logging.getLogger(__name__)
class SimpleRadio(object):
related_object_field = None
@ -148,6 +151,37 @@ class CustomRadio(SessionRadio):
return data
@registry.register(name="custom_multiple")
class CustomMultiple(SessionRadio):
"""
Receive a vuejs generated config and use it to launch a radio session
"""
config = serializers.JSONField(required=True)
def get_config(self, data):
return data["config"]
def get_queryset_kwargs(self):
kwargs = super().get_queryset_kwargs()
kwargs["config"] = self.session.config
return kwargs
def validate_session(self, data, **context):
data = super().validate_session(data, **context)
try:
data["config"] is not None
except KeyError:
raise serializers.ValidationError(
"You must provide a configuration for this radio"
)
return data
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs)
return filters.run(kwargs["config"], candidates=qs)
class RelatedObjectRadio(SessionRadio):
"""Abstract radio related to an object (tag, artist, user...)"""

View File

@ -66,6 +66,7 @@ class RadioSessionSerializer(serializers.ModelSerializer):
"user",
"creation_date",
"custom_radio",
"config",
)
def validate(self, data):

View File

@ -413,3 +413,19 @@ def test_get_choices_for_custom_radio_exclude_tag(factories):
expected = [u.track.pk for u in included_uploads]
assert list(choices.values_list("id", flat=True)) == expected
def test_can_start_custom_multiple_radio_from_api(api_client, factories):
tracks = factories["music.Track"].create_batch(5)
url = reverse("api:v1:radios:sessions-list")
map_filters_to_type = {"tags": "names", "artists": "ids"}
for (key, value) in map_filters_to_type.items():
attr = value[:-1]
track_filter_key = [getattr(a.artist, attr) for a in tracks]
config = {"filters": [{"type": key, value: track_filter_key}]}
response = api_client.post(
url,
{"radio_type": "custom_multiple", "config": config},
format="json",
)
assert response.status_code == 201

View File

@ -40,5 +40,6 @@ def test_tag_radio_repr(factories, to_api_date):
"user": session.user.pk,
"related_object_id": tag.name,
"creation_date": to_api_date(session.creation_date),
"config": None,
}
assert serializers.RadioSessionSerializer(session).data == expected

View File

@ -0,0 +1 @@
Adding support for play all radio in search result page (#1563)

View File

@ -7,16 +7,7 @@
class="ui feed icon"
role="button"
/>
<template v-if="running">
<translate translate-context="*/Player/Button.Label/Short, Verb">
Stop radio
</translate>
</template>
<template v-else>
<translate translate-context="*/Queue/Button.Label/Short, Verb">
Play radio
</translate>
</template>
{{ buttonLabel }}
</button>
</template>
@ -28,7 +19,8 @@ export default {
customRadioId: { type: Number, required: false, default: null },
type: { type: String, required: false, default: '' },
clientOnly: { type: Boolean, default: false },
objectId: { type: [String, Number, Object], default: null }
objectId: { type: [String, Number, Object], default: null },
config: { type: [Array, Object], required: false, default: null }
},
computed: {
running () {
@ -39,6 +31,25 @@ export default {
} else {
return current.type === this.type && lodash.isEqual(current.objectId, this.objectId) && current.customRadioId === this.customRadioId
}
},
label () {
return this.config?.[0]?.type ?? null
},
buttonLabel () {
switch (this.label) {
case 'tag':
return this.running
? this.$pgettext('*/Player/Button.Label/Short, Verb', 'Stop tags radio')
: this.$pgettext('*/Player/Button.Label/Short, Verb', 'Start tags radio')
case 'artist':
return this.running
? this.$pgettext('*/Player/Button.Label/Short, Verb', 'Stop artists radio')
: this.$pgettext('*/Player/Button.Label/Short, Verb', 'Start artists radio')
default:
return this.running
? this.$pgettext('*/Player/Button.Label/Short, Verb', 'Stop radio')
: this.$pgettext('*/Queue/Button.Label/Short, Verb', 'Play radio')
}
}
},
methods: {
@ -50,7 +61,8 @@ export default {
type: this.type,
objectId: this.objectId,
customRadioId: this.customRadioId,
clientOnly: this.clientOnly
clientOnly: this.clientOnly,
config: this.config
})
}
}

View File

@ -48,14 +48,15 @@ export default {
}
},
actions: {
start ({ commit, dispatch }, { type, objectId, customRadioId, clientOnly }) {
start ({ commit, dispatch }, { type, objectId, customRadioId, clientOnly, config }) {
const params = {
radio_type: type,
related_object_id: objectId,
custom_radio: customRadioId
custom_radio: customRadioId,
config: config
}
if (clientOnly) {
commit('current', { type, objectId, customRadioId, clientOnly })
commit('current', { type, objectId, customRadioId, clientOnly, config })
commit('running', true)
dispatch('populateQueue', true)
return

View File

@ -23,29 +23,41 @@
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
</label>
</h2>
<form
class="ui form"
@submit.prevent="page = 1; search()"
>
<div class="ui field">
<div class="ui action input">
<input
id="query"
v-model="query"
class="ui input"
name="query"
type="text"
>
<button
:aria-label="labels.submitSearch"
type="submit"
class="ui icon button"
>
<i class="search icon" />
</button>
</div>
<div class="ui two column doubling stackable grid container">
<div class="column">
<form
class="ui form"
@submit.prevent="page = 1; search()"
>
<div class="ui field">
<div class="ui action input">
<input
id="query"
v-model="query"
class="ui input"
name="query"
type="text"
>
<button
:aria-label="labels.submitSearch"
type="submit"
class="ui icon button"
>
<i class="search icon" />
</button>
</div>
</div>
</form>
</div>
</form>
<div class="column">
<radio-button
v-if="currentResults && currentConfigValidated && ( type === 'tags' || type === 'artists' ) "
class="ui right floated medium button"
type="custom_multiple"
:config="currentConfig"
/>
</div>
</div>
<div class="ui secondary pointing menu">
<a
v-for="t in types"
@ -143,6 +155,7 @@ import AlbumCard from '@/components/audio/album/Card.vue'
import TrackTable from '@/components/audio/track/Table.vue'
import Pagination from '@/components/Pagination.vue'
import PlaylistCardList from '@/components/playlists/CardList.vue'
import RadioButton from '@/components/radios/Button.vue'
import RadioCard from '@/components/radios/Card.vue'
import TagsList from '@/components/tags/List.vue'
@ -157,6 +170,7 @@ export default {
Pagination,
PlaylistCardList,
RadioCard,
RadioButton,
TagsList
},
props: {
@ -181,7 +195,8 @@ export default {
series: null
},
isLoading: false,
paginateBy: 25
paginateBy: 25,
config: null
}
},
computed: {
@ -262,6 +277,15 @@ export default {
},
currentResults () {
return this.results[this.currentType.id]
},
currentConfig () {
const resultDict = this.currentResults.results
return this.generateConfig(this.currentType.id, resultDict)
},
currentConfigValidated () {
const configValidate = this.currentConfig
const array = configValidate[0][Object.keys(configValidate[0])[1]]
return array.length >= 1
}
},
watch: {
@ -325,6 +349,29 @@ export default {
type: this.type
}).toString()
)
},
generateConfig: function (type, resultDict) {
const obj = {
type: type.slice(0, -1)
}
switch (type) {
case 'tags':
obj.names = this.generateTagConfig(resultDict, type)
break
case 'artists':
obj.ids = this.generateArtistConfig(resultDict, type)
break
default:
console.info('This type is not yet supported for radio')
obj.ids = 0
}
return [obj]
},
generateTagConfig: function (resultDict, type) {
return Object.values(resultDict).map(({ name }) => name)
},
generateArtistConfig: function (resultDict, type) {
return Object.values(resultDict).map(({ id }) => id)
}
}
}