kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
New radios: play your own content, or a given library
rodzic
a89eb8db6e
commit
2090806398
|
@ -204,6 +204,7 @@ class APIActorSerializer(serializers.ModelSerializer):
|
|||
"type",
|
||||
"manually_approves_followers",
|
||||
"full_username",
|
||||
"is_local",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@ from django.db import connection
|
|||
from django.db.models import Q
|
||||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.federation import fields as federation_fields
|
||||
from funkwhale_api.moderation import filters as moderation_filters
|
||||
from funkwhale_api.music.models import Artist, Track
|
||||
from funkwhale_api.music.models import Artist, Library, Track, Upload
|
||||
from funkwhale_api.tags.models import Tag
|
||||
|
||||
from . import filters, models
|
||||
from .registries import registry
|
||||
|
||||
|
@ -271,3 +272,47 @@ class LessListenedRadio(SessionRadio):
|
|||
qs = super().get_queryset(**kwargs)
|
||||
listened = self.session.user.listenings.all().values_list("track", flat=True)
|
||||
return qs.exclude(pk__in=listened).order_by("?")
|
||||
|
||||
|
||||
@registry.register(name="actor_content")
|
||||
class ActorContentRadio(RelatedObjectRadio):
|
||||
"""
|
||||
Play content from given actor libraries
|
||||
"""
|
||||
|
||||
model = federation_models.Actor
|
||||
related_object_field = federation_fields.ActorRelatedField(required=True)
|
||||
|
||||
def get_related_object(self, value):
|
||||
return value
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
actor_uploads = Upload.objects.filter(
|
||||
library__actor=self.session.related_object,
|
||||
)
|
||||
return qs.filter(pk__in=actor_uploads.values("track"))
|
||||
|
||||
def get_related_object_id_repr(self, obj):
|
||||
return obj.full_username
|
||||
|
||||
|
||||
@registry.register(name="library")
|
||||
class LibraryRadio(RelatedObjectRadio):
|
||||
"""
|
||||
Play content from a given library
|
||||
"""
|
||||
|
||||
model = Library
|
||||
related_object_field = serializers.UUIDField(required=True)
|
||||
|
||||
def get_related_object(self, value):
|
||||
return Library.objects.get(uuid=value)
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
actor_uploads = Upload.objects.filter(library=self.session.related_object,)
|
||||
return qs.filter(pk__in=actor_uploads.values("track"))
|
||||
|
||||
def get_related_object_id_repr(self, obj):
|
||||
return obj.uuid
|
||||
|
|
|
@ -47,6 +47,28 @@ def test_can_pick_by_weight():
|
|||
assert picks[2] > picks[1]
|
||||
|
||||
|
||||
def test_session_radio_excludes_previous_picks(factories):
|
||||
tracks = factories["music.Track"].create_batch(5)
|
||||
user = factories["users.User"]()
|
||||
previous_choices = []
|
||||
for i in range(5):
|
||||
TrackFavorite.add(track=random.choice(tracks), user=user)
|
||||
|
||||
radio = radios.SessionRadio()
|
||||
radio.radio_type = "favorites"
|
||||
radio.start_session(user)
|
||||
|
||||
for i in range(5):
|
||||
pick = radio.pick(user=user, filter_playable=False)
|
||||
assert pick in tracks
|
||||
assert pick not in previous_choices
|
||||
previous_choices.append(pick)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
# no more picks available
|
||||
radio.pick(user=user, filter_playable=False)
|
||||
|
||||
|
||||
def test_can_get_choices_for_favorites_radio(factories):
|
||||
files = factories["music.Upload"].create_batch(10)
|
||||
tracks = [f.track for f in files]
|
||||
|
@ -213,6 +235,77 @@ def test_can_start_tag_radio(factories):
|
|||
assert radio.pick(filter_playable=False) in good_tracks
|
||||
|
||||
|
||||
def test_can_start_actor_content_radio(factories):
|
||||
actor_library = factories["music.Library"](actor__local=True)
|
||||
good_tracks = [
|
||||
factories["music.Upload"](playable=True, library=actor_library).track,
|
||||
factories["music.Upload"](playable=True, library=actor_library).track,
|
||||
factories["music.Upload"](playable=True, library=actor_library).track,
|
||||
]
|
||||
factories["music.Upload"].create_batch(3, playable=True)
|
||||
|
||||
radio = radios.ActorContentRadio()
|
||||
session = radio.start_session(
|
||||
actor_library.actor.user, related_object=actor_library.actor
|
||||
)
|
||||
assert session.radio_type == "actor_content"
|
||||
|
||||
for i in range(3):
|
||||
assert radio.pick() in good_tracks
|
||||
|
||||
|
||||
def test_can_start_actor_content_radio_from_api(
|
||||
logged_in_api_client, preferences, factories
|
||||
):
|
||||
actor = factories["federation.Actor"]()
|
||||
url = reverse("api:v1:radios:sessions-list")
|
||||
|
||||
response = logged_in_api_client.post(
|
||||
url, {"radio_type": "actor_content", "related_object_id": actor.full_username}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
session = models.RadioSession.objects.latest("id")
|
||||
|
||||
assert session.radio_type == "actor_content"
|
||||
assert session.related_object == actor
|
||||
|
||||
|
||||
def test_can_start_library_radio(factories):
|
||||
user = factories["users.User"]()
|
||||
library = factories["music.Library"]()
|
||||
good_tracks = [
|
||||
factories["music.Upload"](library=library).track,
|
||||
factories["music.Upload"](library=library).track,
|
||||
factories["music.Upload"](library=library).track,
|
||||
]
|
||||
factories["music.Upload"].create_batch(3)
|
||||
|
||||
radio = radios.LibraryRadio()
|
||||
session = radio.start_session(user, related_object=library)
|
||||
assert session.radio_type == "library"
|
||||
|
||||
for i in range(3):
|
||||
assert radio.pick(filter_playable=False) in good_tracks
|
||||
|
||||
|
||||
def test_can_start_library_radio_from_api(logged_in_api_client, preferences, factories):
|
||||
library = factories["music.Library"]()
|
||||
url = reverse("api:v1:radios:sessions-list")
|
||||
|
||||
response = logged_in_api_client.post(
|
||||
url, {"radio_type": "library", "related_object_id": library.uuid}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
session = models.RadioSession.objects.latest("id")
|
||||
|
||||
assert session.radio_type == "library"
|
||||
assert session.related_object == library
|
||||
|
||||
|
||||
def test_can_start_artist_radio_from_api(logged_in_api_client, preferences, factories):
|
||||
artist = factories["music.Artist"]()
|
||||
url = reverse("api:v1:radios:sessions-list")
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Added two new radios to play your own content or a given library tracks
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
<library-card
|
||||
:display-scan="false"
|
||||
:display-follow="$store.state.auth.authenticated"
|
||||
:display-follow="$store.state.auth.authenticated && library.actor.full_username != $store.state.auth.fullUsername"
|
||||
:library="library"
|
||||
:display-copy-fid="true"
|
||||
v-for="library in libraries"
|
||||
|
@ -48,16 +48,16 @@ export default {
|
|||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
this.fetchData(this.url)
|
||||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
fetchData (url) {
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let params = _.clone({})
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(this.url, {params: params}).then((response) => {
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
self.previousPage = response.data.previous
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<translate translate-context="Content/Radio/Title">Instance radios</translate>
|
||||
</h3>
|
||||
<div class="ui cards">
|
||||
<radio-card v-if="isAuthenticated" :type="'actor_content'" :object-id="$store.state.auth.fullUsername"></radio-card>
|
||||
<radio-card v-if="isAuthenticated && hasFavorites" :type="'favorites'"></radio-card>
|
||||
<radio-card :type="'random'"></radio-card>
|
||||
<radio-card v-if="$store.state.auth.authenticated" :type="'less-listened'"></radio-card>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="extra content">
|
||||
<user-link v-if="radio.user" :user="radio.user" class="left floated" />
|
||||
<div class="ui hidden divider"></div>
|
||||
<radio-button class="right floated button" :type="type" :custom-radio-id="customRadioId"></radio-button>
|
||||
<radio-button class="right floated button" :type="type" :custom-radio-id="customRadioId" :object-id="objectId"></radio-button>
|
||||
<router-link
|
||||
class="ui basic yellow button right floated"
|
||||
v-if="$store.state.auth.authenticated && type === 'custom' && radio.user.id === $store.state.auth.profile.id"
|
||||
|
@ -33,7 +33,8 @@ import RadioButton from './Button'
|
|||
export default {
|
||||
props: {
|
||||
type: {type: String, required: true},
|
||||
customRadio: {required: false}
|
||||
customRadio: {required: false},
|
||||
objectId: {required: false},
|
||||
},
|
||||
components: {
|
||||
RadioButton
|
||||
|
|
|
@ -10,6 +10,10 @@ export default {
|
|||
getters: {
|
||||
types: state => {
|
||||
return {
|
||||
actor_content: {
|
||||
name: 'Your content',
|
||||
description: "Picks from your own libraries"
|
||||
},
|
||||
random: {
|
||||
name: 'Random',
|
||||
description: "Totally random picks, maybe you'll discover new things?"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<div class="column">
|
||||
<h3 class="ui header"><translate translate-context="Content/Library/Title">Current library</translate></h3>
|
||||
<library-card :library="library" />
|
||||
<radio-button :type="'library'" :object-id="library.uuid"></radio-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
|
@ -12,12 +13,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import RadioButton from '@/components/radios/Button'
|
||||
import LibraryCard from './Card'
|
||||
|
||||
export default {
|
||||
props: ['library'],
|
||||
components: {
|
||||
LibraryCard
|
||||
LibraryCard,
|
||||
RadioButton,
|
||||
},
|
||||
computed: {
|
||||
links () {
|
||||
|
|
|
@ -93,7 +93,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="displayFollow" :class="['ui', 'bottom', {two: library.follow}, 'attached', 'buttons']">
|
||||
<div v-if="displayFollow || radioPlayable" :class="['ui', {two: displayFollow && radioPlayable}, 'bottom', 'attached', 'buttons']">
|
||||
<radio-button v-if="radioPlayable" :type="'library'" :object-id="library.uuid"></radio-button>
|
||||
<template v-if="displayFollow">
|
||||
<button
|
||||
v-if="!library.follow"
|
||||
@click="follow()"
|
||||
|
@ -112,10 +114,6 @@
|
|||
</button>
|
||||
</template>
|
||||
<template v-else-if="library.follow.approved">
|
||||
<button
|
||||
class="ui disabled button"><i class="check icon"></i>
|
||||
<translate translate-context="Content/Library/Card.Paragraph">Following</translate>
|
||||
</button>
|
||||
<dangerous-button
|
||||
color=""
|
||||
:class="['ui', 'button']"
|
||||
|
@ -128,12 +126,14 @@
|
|||
<div slot="modal-confirm"><translate translate-context="*/Library/Button.Label/Verb">Unfollow</translate></div>
|
||||
</dangerous-button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
import RadioButton from '@/components/radios/Button'
|
||||
import jQuery from 'jquery'
|
||||
|
||||
export default {
|
||||
|
@ -144,6 +144,9 @@ export default {
|
|||
displayScan: {type: Boolean, default: true},
|
||||
displayCopyFid: {type: Boolean, default: true},
|
||||
},
|
||||
components: {
|
||||
RadioButton
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isLoadingFollow: false,
|
||||
|
@ -195,7 +198,13 @@ export default {
|
|||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
},
|
||||
radioPlayable () {
|
||||
return (
|
||||
(this.library.actor.is_local || this.scanStatus === 'finished') &&
|
||||
(this.library.privacy_level === 'everyone' || (this.library.follow && this.library.follow.is_approved))
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
launchScan () {
|
||||
|
|
Ładowanie…
Reference in New Issue