Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
funkwhale
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Alexandra Dupouy
funkwhale
Commits
a865fcdc
Verified
Commit
a865fcdc
authored
Oct 02, 2018
by
Agate
💬
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix #551: Added a library widget to display libraries associated with a track, album and artist
parent
f2812c67
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
210 additions
and
10 deletions
+210
-10
api/funkwhale_api/music/views.py
api/funkwhale_api/music/views.py
+39
-1
api/tests/music/test_views.py
api/tests/music/test_views.py
+31
-0
changes/changelog.d/551.enhancement
changes/changelog.d/551.enhancement
+1
-0
front/src/components/common/CopyInput.vue
front/src/components/common/CopyInput.vue
+5
-2
front/src/components/federation/LibraryWidget.vue
front/src/components/federation/LibraryWidget.vue
+84
-0
front/src/components/library/Album.vue
front/src/components/library/Album.vue
+11
-1
front/src/components/library/Artist.vue
front/src/components/library/Artist.vue
+11
-1
front/src/components/library/Track.vue
front/src/components/library/Track.vue
+11
-1
front/src/views/content/remote/Card.vue
front/src/views/content/remote/Card.vue
+17
-4
No files found.
api/funkwhale_api/music/views.py
View file @
a865fcdc
...
...
@@ -3,7 +3,7 @@ import urllib
from
django.conf
import
settings
from
django.db
import
transaction
from
django.db.models
import
Count
,
Prefetch
,
Sum
,
F
from
django.db.models
import
Count
,
Prefetch
,
Sum
,
F
,
Q
from
django.db.models.functions
import
Length
from
django.utils
import
timezone
...
...
@@ -26,6 +26,28 @@ from . import filters, models, serializers, tasks, utils
logger
=
logging
.
getLogger
(
__name__
)
def
get_libraries
(
filter_uploads
):
def
view
(
self
,
request
,
*
args
,
**
kwargs
):
obj
=
self
.
get_object
()
actor
=
utils
.
get_actor_from_request
(
request
)
uploads
=
models
.
Upload
.
objects
.
all
()
uploads
=
filter_uploads
(
obj
,
uploads
)
uploads
=
uploads
.
playable_by
(
actor
)
libraries
=
models
.
Library
.
objects
.
filter
(
pk__in
=
uploads
.
values_list
(
"library"
,
flat
=
True
)
)
libraries
=
libraries
.
select_related
(
"actor"
)
page
=
self
.
paginate_queryset
(
libraries
)
if
page
is
not
None
:
serializer
=
federation_api_serializers
.
LibrarySerializer
(
page
,
many
=
True
)
return
self
.
get_paginated_response
(
serializer
.
data
)
serializer
=
federation_api_serializers
.
LibrarySerializer
(
libraries
,
many
=
True
)
return
Response
(
serializer
.
data
)
return
view
class
TagViewSetMixin
(
object
):
def
get_queryset
(
self
):
queryset
=
super
().
get_queryset
()
...
...
@@ -50,6 +72,14 @@ class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
)
return
queryset
.
prefetch_related
(
Prefetch
(
"albums"
,
queryset
=
albums
)).
distinct
()
libraries
=
detail_route
(
methods
=
[
"get"
])(
get_libraries
(
filter_uploads
=
lambda
o
,
uploads
:
uploads
.
filter
(
Q
(
track__artist
=
o
)
|
Q
(
track__album__artist
=
o
)
)
)
)
class
AlbumViewSet
(
viewsets
.
ReadOnlyModelViewSet
):
queryset
=
(
...
...
@@ -76,6 +106,10 @@ class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
qs
=
queryset
.
prefetch_related
(
Prefetch
(
"tracks"
,
queryset
=
tracks
))
return
qs
.
distinct
()
libraries
=
detail_route
(
methods
=
[
"get"
])(
get_libraries
(
filter_uploads
=
lambda
o
,
uploads
:
uploads
.
filter
(
track__album
=
o
))
)
class
LibraryViewSet
(
mixins
.
CreateModelMixin
,
...
...
@@ -197,6 +231,10 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer
=
serializers
.
LyricsSerializer
(
lyrics
)
return
Response
(
serializer
.
data
)
libraries
=
detail_route
(
methods
=
[
"get"
])(
get_libraries
(
filter_uploads
=
lambda
o
,
uploads
:
uploads
.
filter
(
track
=
o
))
)
def
get_file_path
(
audio_file
):
serve_path
=
settings
.
MUSIC_DIRECTORY_SERVE_PATH
...
...
api/tests/music/test_views.py
View file @
a865fcdc
...
...
@@ -449,3 +449,34 @@ def test_user_can_list_own_library_follows(factories, logged_in_api_client):
"previous"
:
None
,
"results"
:
[
federation_api_serializers
.
LibraryFollowSerializer
(
follow
).
data
],
}
@
pytest
.
mark
.
parametrize
(
"entity"
,
[
"artist"
,
"album"
,
"track"
])
def
test_can_get_libraries_for_music_entities
(
factories
,
api_client
,
entity
,
preferences
):
preferences
[
"common__api_authentication_required"
]
=
False
upload
=
factories
[
"music.Upload"
](
playable
=
True
)
# another private library that should not appear
factories
[
"music.Upload"
](
import_status
=
"finished"
,
library__privacy_level
=
"me"
,
track
=
upload
.
track
).
library
library
=
upload
.
library
data
=
{
"artist"
:
upload
.
track
.
artist
,
"album"
:
upload
.
track
.
album
,
"track"
:
upload
.
track
,
}
url
=
reverse
(
"api:v1:{}s-libraries"
.
format
(
entity
),
kwargs
=
{
"pk"
:
data
[
entity
].
pk
})
response
=
api_client
.
get
(
url
)
expected
=
federation_api_serializers
.
LibrarySerializer
(
library
).
data
assert
response
.
status_code
==
200
assert
response
.
data
==
{
"count"
:
1
,
"next"
:
None
,
"previous"
:
None
,
"results"
:
[
expected
],
}
changes/changelog.d/551.enhancement
0 → 100644
View file @
a865fcdc
Added a library widget to display libraries associated with a track, album and artist (#551)
front/src/components/common/CopyInput.vue
View file @
a865fcdc
...
...
@@ -4,7 +4,7 @@
<translate>
Text copied to clipboard!
</translate>
</p>
<input
ref=
"input"
:value=
"value"
type=
"text"
>
<button
@
click=
"copy"
class=
"ui teal right labeled icon button
"
>
<button
@
click=
"copy"
:class=
"['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']
"
>
<i
class=
"copy icon"
></i>
<translate>
Copy
</translate>
</button>
...
...
@@ -12,7 +12,10 @@
</
template
>
<
script
>
export
default
{
props
:
[
'
value
'
],
props
:
{
value
:
{
type
:
String
},
buttonClasses
:
{
type
:
String
,
default
:
'
teal
'
}
},
data
()
{
return
{
copied
:
false
,
...
...
front/src/components/federation/LibraryWidget.vue
0 → 100644
View file @
a865fcdc
<
template
>
<div
class=
"wrapper"
>
<h3
class=
"ui header"
>
<slot
name=
"title"
></slot>
</h3>
<p
v-if=
"!isLoading && libraries.length > 0"
class=
"ui subtitle"
><slot
name=
"subtitle"
></slot></p>
<p
v-if=
"!isLoading && libraries.length === 0"
class=
"ui subtitle"
><translate>
No matching library.
</translate></p>
<i
@
click=
"fetchData(previousPage)"
:disabled=
"!previousPage"
:class=
"['ui',
{disabled: !previousPage}, 'circular', 'medium', 'angle left', 'icon']">
</i>
<i
@
click=
"fetchData(nextPage)"
:disabled=
"!nextPage"
:class=
"['ui',
{disabled: !nextPage}, 'circular', 'medium', 'angle right', 'icon']">
</i>
<div
class=
"ui hidden divider"
></div>
<div
class=
"ui three cards"
>
<div
v-if=
"isLoading"
class=
"ui inverted active dimmer"
>
<div
class=
"ui loader"
></div>
</div>
<library-card
:display-scan=
"false"
:display-follow=
"$store.state.auth.authenticated"
:library=
"library"
:display-copy-fid=
"true"
v-for=
"library in libraries"
:key=
"library.uuid"
></library-card>
</div>
</div>
</
template
>
<
script
>
import
_
from
'
lodash
'
import
axios
from
'
axios
'
import
LibraryCard
from
'
@/views/content/remote/Card
'
export
default
{
props
:
{
url
:
{
type
:
String
,
required
:
true
}
},
components
:
{
LibraryCard
},
data
()
{
return
{
libraries
:
[],
limit
:
6
,
isLoading
:
false
,
errors
:
null
,
previousPage
:
null
,
nextPage
:
null
}
},
created
()
{
this
.
fetchData
()
},
methods
:
{
fetchData
()
{
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
)
=>
{
self
.
previousPage
=
response
.
data
.
previous
self
.
nextPage
=
response
.
data
.
next
self
.
isLoading
=
false
self
.
libraries
=
response
.
data
.
results
},
error
=>
{
self
.
isLoading
=
false
self
.
errors
=
error
.
backendErrors
})
},
updateOffset
(
increment
)
{
if
(
increment
)
{
this
.
offset
+=
this
.
limit
}
else
{
this
.
offset
=
Math
.
max
(
this
.
offset
-
this
.
limit
,
0
)
}
}
},
watch
:
{
offset
()
{
this
.
fetchData
()
}
}
}
</
script
>
front/src/components/library/Album.vue
View file @
a865fcdc
...
...
@@ -45,6 +45,14 @@
</h2>
<track-table
v-if=
"album"
:artist=
"album.artist"
:display-position=
"true"
:tracks=
"album.tracks"
></track-table>
</div>
<div
class=
"ui vertical stripe segment"
>
<h2>
<translate>
User libraries
</translate>
</h2>
<library-widget
:url=
"'albums/' + id + '/libraries/'"
>
<translate
slot=
"subtitle"
>
This album is present in the following libraries:
</translate>
</library-widget>
</div>
</
template
>
</div>
</template>
...
...
@@ -55,6 +63,7 @@ import logger from '@/logging'
import
backend
from
'
@/audio/backend
'
import
PlayButton
from
'
@/components/audio/PlayButton
'
import
TrackTable
from
'
@/components/audio/track/Table
'
import
LibraryWidget
from
'
@/components/federation/LibraryWidget
'
const
FETCH_URL
=
'
albums/
'
...
...
@@ -62,7 +71,8 @@ export default {
props
:
[
'
id
'
],
components
:
{
PlayButton
,
TrackTable
TrackTable
,
LibraryWidget
},
data
()
{
return
{
...
...
front/src/components/library/Artist.vue
View file @
a865fcdc
...
...
@@ -56,6 +56,14 @@
</h2>
<track-table
:display-position=
"true"
:tracks=
"tracks"
></track-table>
</div>
<div
class=
"ui vertical stripe segment"
>
<h2>
<translate>
User libraries
</translate>
</h2>
<library-widget
:url=
"'artists/' + id + '/libraries/'"
>
<translate
slot=
"subtitle"
>
This artist is present in the following libraries:
</translate>
</library-widget>
</div>
</
template
>
</div>
</template>
...
...
@@ -69,6 +77,7 @@ import AlbumCard from '@/components/audio/album/Card'
import
RadioButton
from
'
@/components/radios/Button
'
import
PlayButton
from
'
@/components/audio/PlayButton
'
import
TrackTable
from
'
@/components/audio/track/Table
'
import
LibraryWidget
from
'
@/components/federation/LibraryWidget
'
export
default
{
props
:
[
'
id
'
],
...
...
@@ -76,7 +85,8 @@ export default {
AlbumCard
,
RadioButton
,
PlayButton
,
TrackTable
TrackTable
,
LibraryWidget
},
data
()
{
return
{
...
...
front/src/components/library/Track.vue
View file @
a865fcdc
...
...
@@ -118,6 +118,14 @@
</a>
</
template
>
</div>
<div
class=
"ui vertical stripe segment"
>
<h2>
<translate>
User libraries
</translate>
</h2>
<library-widget
:url=
"'tracks/' + id + '/libraries/'"
>
<translate
slot=
"subtitle"
>
This track is present in the following libraries:
</translate>
</library-widget>
</div>
</template>
</div>
</template>
...
...
@@ -131,6 +139,7 @@ import logger from '@/logging'
import
PlayButton
from
'
@/components/audio/PlayButton
'
import
TrackFavoriteIcon
from
'
@/components/favorites/TrackFavoriteIcon
'
import
TrackPlaylistIcon
from
'
@/components/playlists/TrackPlaylistIcon
'
import
LibraryWidget
from
'
@/components/federation/LibraryWidget
'
const
FETCH_URL
=
'
tracks/
'
...
...
@@ -139,7 +148,8 @@ export default {
components
:
{
PlayButton
,
TrackPlaylistIcon
,
TrackFavoriteIcon
TrackFavoriteIcon
,
LibraryWidget
},
data
()
{
return
{
...
...
front/src/views/content/remote/Card.vue
View file @
a865fcdc
...
...
@@ -26,7 +26,7 @@
<i
class=
"music icon"
></i>
<translate
:translate-params=
"
{count: library.uploads_count}" :translate-n="library.uploads_count" translate-plural="%{ count } tracks">%{ count } tracks
</translate>
</div>
<div
v-if=
"latestScan"
class=
"meta"
>
<div
v-if=
"
displayScan &&
latestScan"
class=
"meta"
>
<template
v-if=
"latestScan.status === 'pending'"
>
<i
class=
"hourglass icon"
></i>
<translate>
Scan pending
</translate>
...
...
@@ -59,7 +59,7 @@
<translate>
Errored tracks:
</translate>
{{ latestScan.errored_files }}
</div>
</div>
<div
v-if=
"canLaunchScan"
class=
"clearfix"
>
<div
v-if=
"
displayScan &&
canLaunchScan"
class=
"clearfix"
>
<span
class=
"right floated link"
@
click=
"launchScan"
>
<translate>
Launch scan
</translate>
<i
class=
"paper plane icon"
/>
</span>
...
...
@@ -68,7 +68,15 @@
<div
class=
"extra content"
>
<actor-link
:actor=
"library.actor"
/>
</div>
<div
class=
"ui bottom attached buttons"
>
<div
v-if=
"displayCopyFid"
class=
"extra content"
>
<div
class=
"ui form"
>
<div
class=
"field"
>
<label><translate>
Sharing link
</translate></label>
<copy-input
:button-classes=
"'basic'"
:value=
"library.fid"
/>
</div>
</div>
</div>
<div
v-if=
"displayFollow"
class=
"ui bottom attached buttons"
>
<button
v-if=
"!library.follow"
@
click=
"follow()"
...
...
@@ -104,7 +112,12 @@
import
axios
from
'
axios
'
export
default
{
props
:
[
'
library
'
],
props
:
{
library
:
{
type
:
Object
,
required
:
true
},
displayFollow
:
{
type
:
Boolean
,
default
:
true
},
displayScan
:
{
type
:
Boolean
,
default
:
true
},
displayCopyFid
:
{
type
:
Boolean
,
default
:
false
},
},
data
()
{
return
{
isLoadingFollow
:
false
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment