New radios: play your own content, or a given library

environments/review-front-340-9n9j9v/deployments/3368
Eliot Berriot 2019-11-27 15:28:21 +01:00
rodzic a89eb8db6e
commit 2090806398
10 zmienionych plików z 200 dodań i 42 usunięć

Wyświetl plik

@ -204,6 +204,7 @@ class APIActorSerializer(serializers.ModelSerializer):
"type",
"manually_approves_followers",
"full_username",
"is_local",
]

Wyświetl plik

@ -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

Wyświetl plik

@ -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")

Wyświetl plik

@ -0,0 +1 @@
Added two new radios to play your own content or a given library tracks

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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

Wyświetl plik

@ -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?"

Wyświetl plik

@ -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 () {

Wyświetl plik

@ -93,40 +93,39 @@
</div>
</div>
</div>
<div v-if="displayFollow" :class="['ui', 'bottom', {two: library.follow}, 'attached', 'buttons']">
<button
v-if="!library.follow"
@click="follow()"
:class="['ui', 'green', {'loading': isLoadingFollow}, 'button']">
<translate translate-context="Content/Library/Card.Button.Label/Verb">Follow</translate>
</button>
<template v-else-if="!library.follow.approved">
<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
class="ui disabled button"><i class="hourglass icon"></i>
<translate translate-context="Content/Library/Card.Paragraph">Follow request pending approval</translate>
v-if="!library.follow"
@click="follow()"
:class="['ui', 'green', {'loading': isLoadingFollow}, 'button']">
<translate translate-context="Content/Library/Card.Button.Label/Verb">Follow</translate>
</button>
<button
@click="unfollow"
class="ui button">
<translate translate-context="Content/Library/Card.Paragraph">Cancel follow request</translate>
</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']"
:action="unfollow">
<translate translate-context="*/Library/Button.Label/Verb">Unfollow</translate>
<p slot="modal-header"><translate translate-context="Popup/Library/Title">Unfollow this library?</translate></p>
<div slot="modal-content">
<p><translate translate-context="Popup/Library/Paragraph">By unfollowing this library, you loose access to its content.</translate></p>
</div>
<div slot="modal-confirm"><translate translate-context="*/Library/Button.Label/Verb">Unfollow</translate></div>
</dangerous-button>
<template v-else-if="!library.follow.approved">
<button
class="ui disabled button"><i class="hourglass icon"></i>
<translate translate-context="Content/Library/Card.Paragraph">Follow request pending approval</translate>
</button>
<button
@click="unfollow"
class="ui button">
<translate translate-context="Content/Library/Card.Paragraph">Cancel follow request</translate>
</button>
</template>
<template v-else-if="library.follow.approved">
<dangerous-button
color=""
:class="['ui', 'button']"
:action="unfollow">
<translate translate-context="*/Library/Button.Label/Verb">Unfollow</translate>
<p slot="modal-header"><translate translate-context="Popup/Library/Title">Unfollow this library?</translate></p>
<div slot="modal-content">
<p><translate translate-context="Popup/Library/Paragraph">By unfollowing this library, you loose access to its content.</translate></p>
</div>
<div slot="modal-confirm"><translate translate-context="*/Library/Button.Label/Verb">Unfollow</translate></div>
</dangerous-button>
</template>
</template>
</div>
</div>
@ -134,6 +133,7 @@
<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 () {