Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
svfusion
funkwhale
Commits
f4a8d0f8
Commit
f4a8d0f8
authored
Jul 17, 2017
by
Eliot Berriot
Browse files
Merge branch 'release/0.2.1'
parents
30d6195e
05ce2ba7
Changes
21
Hide whitespace changes
Inline
Side-by-side
.gitlab-ci.yml
View file @
f4a8d0f8
...
...
@@ -80,6 +80,21 @@ docker_develop:
tags
:
-
dind
build_api
:
# Simply publish a zip containing api/ directory
stage
:
deploy
image
:
busybox
artifacts
:
name
:
"
api_${CI_COMMIT_REF_NAME}"
paths
:
-
api
script
:
echo Done!
only
:
-
tags
-
master
-
develop
docker_release
:
stage
:
deploy
before_script
:
...
...
api/config/settings/common.py
View file @
f4a8d0f8
...
...
@@ -260,6 +260,18 @@ BROKER_URL = env("CELERY_BROKER_URL", default='django://')
########## END CELERY
CACHES
=
{
"default"
:
{
"BACKEND"
:
"django_redis.cache.RedisCache"
,
"LOCATION"
:
"{0}/{1}"
.
format
(
env
.
cache_url
(
'REDIS_URL'
,
default
=
"redis://127.0.0.1:6379"
),
0
),
"OPTIONS"
:
{
"CLIENT_CLASS"
:
"django_redis.client.DefaultClient"
,
"IGNORE_EXCEPTIONS"
:
True
,
# mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
}
}
}
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
ADMIN_URL
=
r
'^admin/'
# Your common stuff: Below this line define 3rd party library settings
...
...
@@ -301,7 +313,8 @@ REST_FRAMEWORK = {
}
ATOMIC_REQUESTS
=
False
USE_X_FORWARDED_HOST
=
True
USE_X_FORWARDED_PORT
=
True
# Wether we should check user permission before serving audio files (meaning
# return an obfuscated url)
# This require a special configuration on the reverse proxy side
...
...
api/config/settings/local.py
View file @
f4a8d0f8
...
...
@@ -28,14 +28,6 @@ EMAIL_PORT = 1025
EMAIL_BACKEND
=
env
(
'DJANGO_EMAIL_BACKEND'
,
default
=
'django.core.mail.backends.console.EmailBackend'
)
# CACHING
# ------------------------------------------------------------------------------
CACHES
=
{
'default'
:
{
'BACKEND'
:
'django.core.cache.backends.locmem.LocMemCache'
,
'LOCATION'
:
''
}
}
# django-debug-toolbar
# ------------------------------------------------------------------------------
...
...
api/config/settings/production.py
View file @
f4a8d0f8
...
...
@@ -100,17 +100,7 @@ DATABASES['default'] = env.db("DATABASE_URL")
# CACHING
# ------------------------------------------------------------------------------
# Heroku URL does not pass the DB number, so we parse it in
CACHES
=
{
"default"
:
{
"BACKEND"
:
"django_redis.cache.RedisCache"
,
"LOCATION"
:
"{0}/{1}"
.
format
(
env
.
cache_url
(
'REDIS_URL'
,
default
=
"redis://127.0.0.1:6379"
),
0
),
"OPTIONS"
:
{
"CLIENT_CLASS"
:
"django_redis.client.DefaultClient"
,
"IGNORE_EXCEPTIONS"
:
True
,
# mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
}
}
}
# LOGGING CONFIGURATION
...
...
api/funkwhale_api/__init__.py
View file @
f4a8d0f8
# -*- coding: utf-8 -*-
__version__
=
'0.2.
0
'
__version__
=
'0.2.
1
'
__version_info__
=
tuple
([
int
(
num
)
if
num
.
isdigit
()
else
num
for
num
in
__version__
.
replace
(
'-'
,
'.'
,
1
).
split
(
'.'
)])
api/funkwhale_api/music/models.py
View file @
f4a8d0f8
...
...
@@ -27,6 +27,7 @@ class APIModelMixin(models.Model):
api_includes
=
[]
creation_date
=
models
.
DateTimeField
(
default
=
timezone
.
now
)
import_hooks
=
[]
class
Meta
:
abstract
=
True
ordering
=
[
'-creation_date'
]
...
...
@@ -291,6 +292,9 @@ class Track(APIModelMixin):
]
tags
=
TaggableManager
()
class
Meta
:
ordering
=
[
'album'
,
'position'
]
def
__str__
(
self
):
return
self
.
title
...
...
@@ -358,6 +362,12 @@ class TrackFile(models.Model):
'api:v1:trackfiles-serve'
,
kwargs
=
{
'pk'
:
self
.
pk
})
return
self
.
audio_file
.
url
@
property
def
filename
(
self
):
return
'{}{}'
.
format
(
self
.
track
.
full_name
,
os
.
path
.
splitext
(
self
.
audio_file
.
name
)[
-
1
])
class
ImportBatch
(
models
.
Model
):
creation_date
=
models
.
DateTimeField
(
default
=
timezone
.
now
)
...
...
@@ -386,6 +396,8 @@ class ImportJob(models.Model):
)
status
=
models
.
CharField
(
choices
=
STATUS_CHOICES
,
default
=
'pending'
,
max_length
=
30
)
class
Meta
:
ordering
=
(
'id'
,
)
@
celery
.
app
.
task
(
name
=
'ImportJob.run'
,
filter
=
celery
.
task_method
)
def
run
(
self
,
replace
=
False
):
try
:
...
...
api/funkwhale_api/music/serializers.py
View file @
f4a8d0f8
...
...
@@ -31,11 +31,20 @@ class ImportBatchSerializer(serializers.ModelSerializer):
model
=
models
.
ImportBatch
fields
=
(
'id'
,
'jobs'
,
'status'
,
'creation_date'
)
class
TrackFileSerializer
(
serializers
.
ModelSerializer
):
path
=
serializers
.
SerializerMethodField
()
class
Meta
:
model
=
models
.
TrackFile
fields
=
(
'id'
,
'path'
,
'duration'
,
'source'
)
fields
=
(
'id'
,
'path'
,
'duration'
,
'source'
,
'filename'
)
def
get_path
(
self
,
o
):
request
=
self
.
context
.
get
(
'request'
)
url
=
o
.
path
if
request
:
url
=
request
.
build_absolute_uri
(
url
)
return
url
class
SimpleAlbumSerializer
(
serializers
.
ModelSerializer
):
...
...
@@ -62,7 +71,15 @@ class TrackSerializer(LyricsMixin):
tags
=
TagSerializer
(
many
=
True
,
read_only
=
True
)
class
Meta
:
model
=
models
.
Track
fields
=
(
'id'
,
'mbid'
,
'title'
,
'artist'
,
'files'
,
'tags'
,
'lyrics'
)
fields
=
(
'id'
,
'mbid'
,
'title'
,
'artist'
,
'files'
,
'tags'
,
'position'
,
'lyrics'
)
class
TrackSerializerNested
(
LyricsMixin
):
artist
=
ArtistSerializer
()
...
...
api/funkwhale_api/music/views.py
View file @
f4a8d0f8
...
...
@@ -139,9 +139,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
return
Response
(
status
=
404
)
response
=
Response
()
filename
=
"filename*=UTF-8''{}{}"
.
format
(
urllib
.
parse
.
quote
(
f
.
track
.
full_name
),
os
.
path
.
splitext
(
f
.
audio_file
.
name
)[
-
1
])
filename
=
"filename*=UTF-8''{}"
.
format
(
urllib
.
parse
.
quote
(
f
.
filename
))
response
[
"Content-Disposition"
]
=
"attachment; {}"
.
format
(
filename
)
response
[
'X-Accel-Redirect'
]
=
"{}{}"
.
format
(
settings
.
PROTECT_FILES_PATH
,
...
...
api/requirements/base.txt
View file @
f4a8d0f8
...
...
@@ -55,4 +55,4 @@ mutagen==1.38
# Until this is merged
git+https://github.com/EliotBerriot/PyMemoize.git@django
django-dynamic-preferences>=1.
2
,<1.
3
django-dynamic-preferences>=1.
3
,<1.
4
deploy/docker-compose.yml
View file @
f4a8d0f8
...
...
@@ -28,7 +28,7 @@ services:
-
C_FORCE_ROOT=true
volumes
:
-
./data/music:/music:ro
-
./
api
/media:/app/funkwhale_api/media
-
./
data
/media:/app/funkwhale_api/media
celerybeat
:
restart
:
unless-stopped
...
...
deploy/nginx.conf
View file @
f4a8d0f8
...
...
@@ -41,6 +41,8 @@ server {
proxy_set_header
X-Real-IP
$remote_addr
;
proxy_set_header
X-Forwarded-For
$proxy_add_x_forwarded_for
;
proxy_set_header
X-Forwarded-Proto
$scheme
;
proxy_set_header
X-Forwarded-Host
$host
:
$server_port
;
proxy_set_header
X-Forwarded-Port
$server_port
;
proxy_redirect
off
;
proxy_pass
http://funkwhale-api/api/
;
}
...
...
dev.yml
View file @
f4a8d0f8
...
...
@@ -63,4 +63,4 @@ services:
-
./docker/nginx/conf.dev:/etc/nginx/nginx.conf
-
./api/funkwhale_api/media:/protected/media
ports
:
-
"
0.0.0.0:6001:
80
"
-
"
0.0.0.0:6001:
6001
"
docker/nginx/conf.dev
View file @
f4a8d0f8
...
...
@@ -28,7 +28,7 @@ http {
#gzip on;
server {
listen
80
;
listen
6001
;
charset utf-8;
location /_protected/media {
...
...
@@ -40,6 +40,8 @@ http {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Port $server_port;
proxy_redirect off;
proxy_pass http://api:12081/;
}
...
...
docs/changelog.rst
View file @
f4a8d0f8
Changelog
=========
0.2.1
-----
2017-07-17
* Now return media files with absolute URL
* Now display CLI instructions to download a set of tracks
* Fixed #33: sort by track position in album in API by default, also reuse that information on frontend side
* More robust audio player and queue in various situations:
* upgrade to latest dynamic_preferences and use redis as cache even locally
0.2
-------
...
...
front/src/audio/index.js
View file @
f4a8d0f8
...
...
@@ -124,9 +124,9 @@ class Audio {
}
play
()
{
logger
.
default
.
info
(
'
Playing track
'
)
if
(
this
.
state
.
startLoad
)
{
if
(
!
this
.
state
.
playing
&&
this
.
$Audio
.
readyState
>=
2
)
{
logger
.
default
.
info
(
'
Playing track
'
)
this
.
$Audio
.
play
()
this
.
state
.
paused
=
false
this
.
state
.
playing
=
true
...
...
front/src/audio/queue.js
View file @
f4a8d0f8
...
...
@@ -123,6 +123,7 @@ class Queue {
this
.
tracks
.
splice
(
index
,
0
,
track
)
}
if
(
this
.
ended
)
{
logger
.
default
.
debug
(
'
Playing appended track
'
)
this
.
play
(
this
.
currentIndex
+
1
)
}
this
.
cache
()
...
...
@@ -152,19 +153,31 @@ class Queue {
clean
()
{
this
.
stop
()
radios
.
stop
()
this
.
tracks
=
[]
this
.
currentIndex
=
-
1
this
.
currentTrack
=
null
// so we replay automatically on next track append
this
.
ended
=
true
}
cleanTrack
(
index
)
{
if
(
index
===
this
.
currentIndex
)
{
// are we removing current playin track
let
current
=
index
===
this
.
currentIndex
if
(
current
)
{
this
.
stop
()
}
if
(
index
<
this
.
currentIndex
)
{
this
.
currentIndex
-=
1
}
this
.
tracks
.
splice
(
index
,
1
)
if
(
current
)
{
// we play next track, which now have the same index
this
.
play
(
index
)
}
if
(
this
.
currentIndex
===
this
.
tracks
.
length
-
1
)
{
this
.
populateFromRadio
()
}
}
stop
()
{
...
...
@@ -172,12 +185,17 @@ class Queue {
this
.
audio
.
destroyed
()
}
play
(
index
)
{
if
(
this
.
audio
.
destroyed
)
{
logger
.
default
.
debug
(
'
Destroying previous audio...
'
)
this
.
audio
.
destroyed
()
let
self
=
this
let
currentIndex
=
index
let
currentTrack
=
this
.
tracks
[
index
]
if
(
!
currentTrack
)
{
logger
.
default
.
debug
(
'
No track at index
'
,
index
)
return
}
this
.
currentIndex
=
index
this
.
currentTrack
=
this
.
tracks
[
index
]
this
.
currentIndex
=
currentIndex
this
.
currentTrack
=
currentTrack
this
.
ended
=
false
let
file
=
this
.
currentTrack
.
files
[
0
]
if
(
!
file
)
{
...
...
@@ -193,7 +211,11 @@ class Queue {
path
=
url
.
updateQueryString
(
path
,
'
jwt
'
,
auth
.
getAuthToken
())
}
this
.
audio
=
new
Audio
(
path
,
{
if
(
this
.
audio
.
destroyed
)
{
logger
.
default
.
debug
(
'
Destroying previous audio...
'
,
index
-
1
)
this
.
audio
.
destroyed
()
}
let
audio
=
new
Audio
(
path
,
{
preload
:
true
,
autoplay
:
true
,
rate
:
1
,
...
...
@@ -201,6 +223,17 @@ class Queue {
volume
:
this
.
state
.
volume
,
onEnded
:
this
.
handleAudioEnded
.
bind
(
this
)
})
this
.
audio
=
audio
audio
.
updateHook
(
'
playState
'
,
function
(
e
)
{
// in some situations, we may have a race condition, for example
// if the user spams the next / previous buttons, with multiple audios
// playing at the same time. To avoid that, we ensure the audio
// still matches de queue current audio
if
(
audio
!==
self
.
audio
)
{
logger
.
default
.
debug
(
'
Destroying duplicate audio
'
)
audio
.
destroyed
()
}
})
if
(
this
.
currentIndex
===
this
.
tracks
.
length
-
1
)
{
this
.
populateFromRadio
()
}
...
...
front/src/components/audio/Player.vue
View file @
f4a8d0f8
...
...
@@ -24,7 +24,7 @@
</div>
</div>
</div>
<div
class=
"progress-area"
>
<div
class=
"progress-area"
v-if=
"queue.currentTrack"
>
<div
class=
"ui grid"
>
<div
class=
"left floated four wide column"
>
<p
class=
"timer start"
@
click=
"queue.audio.setTime(0)"
>
{{
queue
.
audio
.
state
.
currentTimeFormat
}}
</p>
...
...
front/src/components/audio/album/Card.vue
View file @
f4a8d0f8
...
...
@@ -22,6 +22,9 @@
</td>
<td
colspan=
"6"
>
<router-link
class=
"track discrete link"
:to=
"
{name: 'library.track', params: {id: track.id }}">
<template
v-if=
"track.position"
>
{{
track
.
position
}}
.
</
template
>
{{ track.title }}
</router-link>
</td>
...
...
front/src/components/audio/track/Table.vue
View file @
f4a8d0f8
...
...
@@ -20,9 +20,12 @@
<img
class=
"ui mini image"
v-else
src=
"../../..//assets/audio/default-cover.png"
>
</td>
<td
colspan=
"6"
>
<router-link
class=
"track"
:to=
"
{name: 'library.track', params: {id: track.id }}">
{{
track
.
title
}}
</router-link>
<router-link
class=
"track"
:to=
"
{name: 'library.track', params: {id: track.id }}">
<template
v-if=
"displayPosition && track.position"
>
{{
track
.
position
}}
.
</
template
>
{{ track.title }}
</router-link>
</td>
<td
colspan=
"6"
>
<router-link
class=
"artist discrete link"
:to=
"{name: 'library.artist', params: {id: track.artist.id }}"
>
...
...
@@ -37,23 +40,70 @@
<td><track-favorite-icon
class=
"favorite-icon"
:track=
"track"
></track-favorite-icon></td>
</tr>
</tbody>
<tfoot
class=
"full-width"
>
<tr>
<th
colspan=
"3"
>
<button
@
click=
"showDownloadModal = !showDownloadModal"
class=
"ui basic button"
>
Download...
</button>
<modal
:show.sync=
"showDownloadModal"
>
<div
class=
"header"
>
Download tracks
</div>
<div
class=
"content"
>
<div
class=
"description"
>
<p>
There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive.
However, you can use a command line tools such as
<a
href=
"https://curl.haxx.se/"
target=
"_blank"
>
cURL
</a>
to easily download a list of tracks.
</p>
<p>
Simply copy paste the snippet below into a terminal to launch the download.
</p>
<div
class=
"ui warning message"
>
Keep your PRIVATE_TOKEN secret as it gives access to your account.
</div>
<pre>
export PRIVATE_TOKEN="{{ auth.getAuthToken ()}}"
<
template
v-for=
"track in tracks"
>
curl -G -o "
{{
track
.
files
[
0
].
filename
}}
"
<template
v-if=
"auth.user.authenticated"
>
--header "Authorization: JWT $PRIVATE_TOKEN"
</
template
>
"{{ backend.absoluteUrl(track.files[0].path) }}"
</template>
</pre>
</div>
</div>
<div
class=
"actions"
>
<div
class=
"ui black deny button"
>
Cancel
</div>
</div>
</modal>
</th>
<th></th>
<th
colspan=
"4"
></th>
<th
colspan=
"6"
></th>
<th
colspan=
"6"
></th>
<th></th>
</tr>
</tfoot>
</table>
</template>
<
script
>
import
backend
from
'
@/audio/backend
'
import
auth
from
'
@/auth
'
import
TrackFavoriteIcon
from
'
@/components/favorites/TrackFavoriteIcon
'
import
PlayButton
from
'
@/components/audio/PlayButton
'
import
Modal
from
'
@/components/semantic/Modal
'
export
default
{
props
:
[
'
tracks
'
],
props
:
{
tracks
:
{
type
:
Array
,
required
:
true
},
displayPosition
:
{
type
:
Boolean
,
default
:
false
}
},
components
:
{
Modal
,
TrackFavoriteIcon
,
PlayButton
},
data
()
{
return
{
backend
:
backend
backend
:
backend
,
auth
:
auth
,
showDownloadModal
:
false
}
}
}
...
...
front/src/components/library/Album.vue
View file @
f4a8d0f8
...
...
@@ -34,7 +34,7 @@
</div>
<div
class=
"ui vertical stripe segment"
>
<h2>
Tracks
</h2>
<track-table
v-if=
"album"
:tracks=
"album.tracks"
></track-table>
<track-table
v-if=
"album"
:display-position=
"true"
:tracks=
"album.tracks"
></track-table>
</div>
</
template
>
</div>
...
...
Prev
1
2
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new 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