Resolve "Add "play all" button in tag search result page"
This commit is contained in:
parent
3d825cd170
commit
0f4226e06f
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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 = [
|
||||||
|
]
|
|
@ -51,6 +51,8 @@ class RadioSession(models.Model):
|
||||||
related_object = GenericForeignKey(
|
related_object = GenericForeignKey(
|
||||||
"related_object_content_type", "related_object_id"
|
"related_object_content_type", "related_object_id"
|
||||||
)
|
)
|
||||||
|
CONFIG_VERSION = 0
|
||||||
|
config = JSONField(encoder=DjangoJSONEncoder, blank=True, null=True)
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
self.radio.clean(self)
|
self.radio.clean(self)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -15,6 +16,8 @@ from funkwhale_api.tags.models import Tag
|
||||||
from . import filters, models
|
from . import filters, models
|
||||||
from .registries import registry
|
from .registries import registry
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SimpleRadio(object):
|
class SimpleRadio(object):
|
||||||
related_object_field = None
|
related_object_field = None
|
||||||
|
@ -148,6 +151,37 @@ class CustomRadio(SessionRadio):
|
||||||
return data
|
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):
|
class RelatedObjectRadio(SessionRadio):
|
||||||
"""Abstract radio related to an object (tag, artist, user...)"""
|
"""Abstract radio related to an object (tag, artist, user...)"""
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,7 @@ class RadioSessionSerializer(serializers.ModelSerializer):
|
||||||
"user",
|
"user",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"custom_radio",
|
"custom_radio",
|
||||||
|
"config",
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|
|
@ -413,3 +413,19 @@ def test_get_choices_for_custom_radio_exclude_tag(factories):
|
||||||
|
|
||||||
expected = [u.track.pk for u in included_uploads]
|
expected = [u.track.pk for u in included_uploads]
|
||||||
assert list(choices.values_list("id", flat=True)) == expected
|
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
|
||||||
|
|
|
@ -40,5 +40,6 @@ def test_tag_radio_repr(factories, to_api_date):
|
||||||
"user": session.user.pk,
|
"user": session.user.pk,
|
||||||
"related_object_id": tag.name,
|
"related_object_id": tag.name,
|
||||||
"creation_date": to_api_date(session.creation_date),
|
"creation_date": to_api_date(session.creation_date),
|
||||||
|
"config": None,
|
||||||
}
|
}
|
||||||
assert serializers.RadioSessionSerializer(session).data == expected
|
assert serializers.RadioSessionSerializer(session).data == expected
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Adding support for play all radio in search result page (#1563)
|
|
@ -7,16 +7,7 @@
|
||||||
class="ui feed icon"
|
class="ui feed icon"
|
||||||
role="button"
|
role="button"
|
||||||
/>
|
/>
|
||||||
<template v-if="running">
|
{{ buttonLabel }}
|
||||||
<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>
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -28,7 +19,8 @@ export default {
|
||||||
customRadioId: { type: Number, required: false, default: null },
|
customRadioId: { type: Number, required: false, default: null },
|
||||||
type: { type: String, required: false, default: '' },
|
type: { type: String, required: false, default: '' },
|
||||||
clientOnly: { type: Boolean, default: false },
|
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: {
|
computed: {
|
||||||
running () {
|
running () {
|
||||||
|
@ -39,6 +31,25 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
return current.type === this.type && lodash.isEqual(current.objectId, this.objectId) && current.customRadioId === this.customRadioId
|
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: {
|
methods: {
|
||||||
|
@ -50,7 +61,8 @@ export default {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
objectId: this.objectId,
|
objectId: this.objectId,
|
||||||
customRadioId: this.customRadioId,
|
customRadioId: this.customRadioId,
|
||||||
clientOnly: this.clientOnly
|
clientOnly: this.clientOnly,
|
||||||
|
config: this.config
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,14 +48,15 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
start ({ commit, dispatch }, { type, objectId, customRadioId, clientOnly }) {
|
start ({ commit, dispatch }, { type, objectId, customRadioId, clientOnly, config }) {
|
||||||
const params = {
|
const params = {
|
||||||
radio_type: type,
|
radio_type: type,
|
||||||
related_object_id: objectId,
|
related_object_id: objectId,
|
||||||
custom_radio: customRadioId
|
custom_radio: customRadioId,
|
||||||
|
config: config
|
||||||
}
|
}
|
||||||
if (clientOnly) {
|
if (clientOnly) {
|
||||||
commit('current', { type, objectId, customRadioId, clientOnly })
|
commit('current', { type, objectId, customRadioId, clientOnly, config })
|
||||||
commit('running', true)
|
commit('running', true)
|
||||||
dispatch('populateQueue', true)
|
dispatch('populateQueue', true)
|
||||||
return
|
return
|
||||||
|
|
|
@ -23,29 +23,41 @@
|
||||||
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
||||||
</label>
|
</label>
|
||||||
</h2>
|
</h2>
|
||||||
<form
|
<div class="ui two column doubling stackable grid container">
|
||||||
class="ui form"
|
<div class="column">
|
||||||
@submit.prevent="page = 1; search()"
|
<form
|
||||||
>
|
class="ui form"
|
||||||
<div class="ui field">
|
@submit.prevent="page = 1; search()"
|
||||||
<div class="ui action input">
|
>
|
||||||
<input
|
<div class="ui field">
|
||||||
id="query"
|
<div class="ui action input">
|
||||||
v-model="query"
|
<input
|
||||||
class="ui input"
|
id="query"
|
||||||
name="query"
|
v-model="query"
|
||||||
type="text"
|
class="ui input"
|
||||||
>
|
name="query"
|
||||||
<button
|
type="text"
|
||||||
:aria-label="labels.submitSearch"
|
>
|
||||||
type="submit"
|
<button
|
||||||
class="ui icon button"
|
:aria-label="labels.submitSearch"
|
||||||
>
|
type="submit"
|
||||||
<i class="search icon" />
|
class="ui icon button"
|
||||||
</button>
|
>
|
||||||
</div>
|
<i class="search icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</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">
|
<div class="ui secondary pointing menu">
|
||||||
<a
|
<a
|
||||||
v-for="t in types"
|
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 TrackTable from '@/components/audio/track/Table.vue'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import PlaylistCardList from '@/components/playlists/CardList.vue'
|
import PlaylistCardList from '@/components/playlists/CardList.vue'
|
||||||
|
import RadioButton from '@/components/radios/Button.vue'
|
||||||
import RadioCard from '@/components/radios/Card.vue'
|
import RadioCard from '@/components/radios/Card.vue'
|
||||||
import TagsList from '@/components/tags/List.vue'
|
import TagsList from '@/components/tags/List.vue'
|
||||||
|
|
||||||
|
@ -157,6 +170,7 @@ export default {
|
||||||
Pagination,
|
Pagination,
|
||||||
PlaylistCardList,
|
PlaylistCardList,
|
||||||
RadioCard,
|
RadioCard,
|
||||||
|
RadioButton,
|
||||||
TagsList
|
TagsList
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -181,7 +195,8 @@ export default {
|
||||||
series: null
|
series: null
|
||||||
},
|
},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
paginateBy: 25
|
paginateBy: 25,
|
||||||
|
config: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -262,6 +277,15 @@ export default {
|
||||||
},
|
},
|
||||||
currentResults () {
|
currentResults () {
|
||||||
return this.results[this.currentType.id]
|
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: {
|
watch: {
|
||||||
|
@ -325,6 +349,29 @@ export default {
|
||||||
type: this.type
|
type: this.type
|
||||||
}).toString()
|
}).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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue