Merge branch 'release/0.6.1'
This commit is contained in:
commit
6bf73384d2
1
.env.dev
1
.env.dev
|
@ -1,4 +1,3 @@
|
|||
API_AUTHENTICATION_REQUIRED=True
|
||||
CACHALOT_ENABLED=False
|
||||
RAVEN_ENABLED=false
|
||||
RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5
|
||||
|
|
|
@ -84,3 +84,5 @@ front/test/unit/coverage
|
|||
front/test/e2e/reports
|
||||
front/selenium-debug.log
|
||||
docs/_build
|
||||
|
||||
data/
|
||||
|
|
21
CHANGELOG
21
CHANGELOG
|
@ -3,6 +3,27 @@ Changelog
|
|||
|
||||
.. towncrier
|
||||
|
||||
0.6.1 (unreleased)
|
||||
------------------
|
||||
|
||||
Features:
|
||||
|
||||
- Can now skip acoustid on file import with the --no-acoustid flag (#111)
|
||||
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Added missing batch id in output during import (#112)
|
||||
- Added some feedback on the play button (#100)
|
||||
- Smarter pagination which takes a fixed size (#84)
|
||||
|
||||
|
||||
Other:
|
||||
|
||||
- Completely removed django-cachalot from the codebase (#110). You can safely
|
||||
remove the CACHALOT_ENABLED setting from your .env file
|
||||
|
||||
|
||||
0.6 (2018-03-04)
|
||||
----------------
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/bash -eux
|
||||
python /app/manage.py collectstatic --noinput
|
||||
/usr/local/bin/daphne --root-path=/app -b 0.0.0.0 -p 5000 config.asgi:application
|
||||
/usr/local/bin/daphne -b 0.0.0.0 -p 5000 config.asgi:application
|
||||
|
|
|
@ -4,16 +4,19 @@ set -e
|
|||
# Since docker-compose relies heavily on environment variables itself for configuration, we'd have to define multiple
|
||||
# environment variables just to support cookiecutter out of the box. That makes no sense, so this little entrypoint
|
||||
# does all this for us.
|
||||
export CACHE_URL=redis://redis:6379/0
|
||||
export CACHE_URL=${CACHE_URL:="redis://redis:6379/0"}
|
||||
|
||||
# the official postgres image uses 'postgres' as default user if not set explictly.
|
||||
if [ -z "$POSTGRES_ENV_POSTGRES_USER" ]; then
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
# the official postgres image uses 'postgres' as default user if not set explictly.
|
||||
if [ -z "$POSTGRES_ENV_POSTGRES_USER" ]; then
|
||||
export POSTGRES_ENV_POSTGRES_USER=postgres
|
||||
fi
|
||||
export DATABASE_URL=postgres://$POSTGRES_ENV_POSTGRES_USER:$POSTGRES_ENV_POSTGRES_PASSWORD@postgres:5432/$POSTGRES_ENV_POSTGRES_USER
|
||||
fi
|
||||
|
||||
export DATABASE_URL=postgres://$POSTGRES_ENV_POSTGRES_USER:$POSTGRES_ENV_POSTGRES_PASSWORD@postgres:5432/$POSTGRES_ENV_POSTGRES_USER
|
||||
|
||||
export CELERY_BROKER_URL=$CACHE_URL
|
||||
if [ -z "$CELERY_BROKER_URL" ]; then
|
||||
export CELERY_BROKER_URL=$CACHE_URL
|
||||
fi
|
||||
|
||||
# we copy the frontend files, if any so we can serve them from the outside
|
||||
if [ -d "frontend" ]; then
|
||||
|
|
|
@ -55,7 +55,6 @@ THIRD_PARTY_APPS = (
|
|||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'taggit',
|
||||
'cachalot',
|
||||
'rest_auth',
|
||||
'rest_auth.registration',
|
||||
'mptt',
|
||||
|
@ -310,7 +309,7 @@ CELERY_BROKER_URL = env(
|
|||
"CELERY_BROKER_URL", default=env('CACHE_URL', default=CACHE_DEFAULT))
|
||||
########## END CELERY
|
||||
# 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
|
||||
CELERY_TASK_DEFAULT_RATE_LIMIT = 1
|
||||
CELERY_TASK_TIME_LIMIT = 300
|
||||
|
@ -371,9 +370,6 @@ MUSICBRAINZ_CACHE_DURATION = env.int(
|
|||
default=300
|
||||
)
|
||||
|
||||
CACHALOT_ENABLED = env.bool('CACHALOT_ENABLED', default=True)
|
||||
|
||||
|
||||
# Custom Admin URL, use {% url 'admin:index' %}
|
||||
ADMIN_URL = env('DJANGO_ADMIN_URL', default='^api/admin/')
|
||||
CSRF_USE_SESSIONS = True
|
||||
|
|
|
@ -6,8 +6,8 @@ python manage.py migrate --noinput
|
|||
|
||||
echo "Creating demo user..."
|
||||
|
||||
cat demo/demo-user.py | python manage.py shell --plain
|
||||
cat demo/demo-user.py | python manage.py shell -i python
|
||||
|
||||
echo "Importing demo tracks..."
|
||||
|
||||
python manage.py import_files "/music/**/*.ogg" --recursive --noinput
|
||||
python manage.py import_files "/music/**/*.ogg" --recursive --noinput --username demo
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
__version__ = '0.6'
|
||||
__version__ = '0.6.1'
|
||||
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
||||
|
|
|
@ -9,6 +9,7 @@ from rest_framework import exceptions
|
|||
from rest_framework_jwt.settings import api_settings
|
||||
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
|
||||
|
||||
from funkwhale_api.users.models import User
|
||||
|
||||
|
||||
class TokenHeaderAuth(BaseJSONWebTokenAuthentication):
|
||||
|
@ -40,7 +41,7 @@ class TokenAuthMiddleware:
|
|||
auth = TokenHeaderAuth()
|
||||
try:
|
||||
user, token = auth.authenticate(scope)
|
||||
except exceptions.AuthenticationFailed:
|
||||
except (User.DoesNotExist, exceptions.AuthenticationFailed):
|
||||
user = AnonymousUser()
|
||||
|
||||
scope['user'] = user
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from django.core.files.base import ContentFile
|
||||
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
from funkwhale_api.taskapp import celery
|
||||
from funkwhale_api.providers.acoustid import get_acoustid_client
|
||||
from funkwhale_api.providers.audiofile.tasks import import_track_data_from_path
|
||||
|
@ -23,21 +25,22 @@ def set_acoustid_on_track_file(track_file):
|
|||
return update(result['id'])
|
||||
|
||||
|
||||
def _do_import(import_job, replace):
|
||||
def _do_import(import_job, replace, use_acoustid=True):
|
||||
from_file = bool(import_job.audio_file)
|
||||
mbid = import_job.mbid
|
||||
acoustid_track_id = None
|
||||
duration = None
|
||||
track = None
|
||||
if not mbid and from_file:
|
||||
manager = global_preferences_registry.manager()
|
||||
use_acoustid = use_acoustid and manager['providers_acoustid__api_key']
|
||||
if not mbid and use_acoustid and from_file:
|
||||
# we try to deduce mbid from acoustid
|
||||
client = get_acoustid_client()
|
||||
match = client.get_best_match(import_job.audio_file.path)
|
||||
if not match:
|
||||
raise ValueError('Cannot get match')
|
||||
duration = match['recordings'][0]['duration']
|
||||
mbid = match['recordings'][0]['id']
|
||||
acoustid_track_id = match['id']
|
||||
if match:
|
||||
duration = match['recordings'][0]['duration']
|
||||
mbid = match['recordings'][0]['id']
|
||||
acoustid_track_id = match['id']
|
||||
if mbid:
|
||||
track, _ = models.Track.get_or_create_from_api(mbid=mbid)
|
||||
else:
|
||||
|
@ -77,13 +80,13 @@ def _do_import(import_job, replace):
|
|||
models.ImportJob.objects.filter(
|
||||
status__in=['pending', 'errored']),
|
||||
'import_job')
|
||||
def import_job_run(self, import_job, replace=False):
|
||||
def import_job_run(self, import_job, replace=False, use_acoustid=True):
|
||||
def mark_errored():
|
||||
import_job.status = 'errored'
|
||||
import_job.save()
|
||||
import_job.save(update_fields=['status'])
|
||||
|
||||
try:
|
||||
return _do_import(import_job, replace)
|
||||
return _do_import(import_job, replace, use_acoustid=use_acoustid)
|
||||
except Exception as exc:
|
||||
if not settings.DEBUG:
|
||||
try:
|
||||
|
|
|
@ -34,6 +34,13 @@ class Command(BaseCommand):
|
|||
default=False,
|
||||
help='Will launch celery tasks for each file to import instead of doing it synchronously and block the CLI',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-acoustid',
|
||||
action='store_true',
|
||||
dest='no_acoustid',
|
||||
default=False,
|
||||
help='Use this flag to completely bypass acoustid completely',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--noinput', '--no-input', action='store_false', dest='interactive',
|
||||
help="Do NOT prompt the user for input of any kind.",
|
||||
|
@ -81,13 +88,12 @@ class Command(BaseCommand):
|
|||
raise CommandError("Import cancelled.")
|
||||
|
||||
batch = self.do_import(matching, user=user, options=options)
|
||||
|
||||
message = 'Successfully imported {} tracks'
|
||||
if options['async']:
|
||||
message = 'Successfully launched import for {} tracks'
|
||||
self.stdout.write(message.format(len(matching)))
|
||||
self.stdout.write(
|
||||
"For details, please refer to import batch #".format(batch.pk))
|
||||
"For details, please refer to import batch #{}".format(batch.pk))
|
||||
|
||||
@transaction.atomic
|
||||
def do_import(self, matching, user, options):
|
||||
|
@ -109,7 +115,10 @@ class Command(BaseCommand):
|
|||
|
||||
job.save()
|
||||
try:
|
||||
utils.on_commit(import_handler, import_job_id=job.pk)
|
||||
utils.on_commit(
|
||||
import_handler,
|
||||
import_job_id=job.pk,
|
||||
use_acoustid=not options['no_acoustid'])
|
||||
except Exception as e:
|
||||
self.stdout.write('Error: {}'.format(e))
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Page Not found{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Page Not found</h1>
|
||||
|
||||
<p>This is not the page you were looking for.</p>
|
||||
{% endblock content %}
|
|
@ -1,13 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Server Error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Ooops!!! 500</h1>
|
||||
|
||||
<h3>Looks like something went wrong!</h3>
|
||||
|
||||
<p>We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.</p>
|
||||
{% endblock content %}
|
||||
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
{% load staticfiles i18n %}<!DOCTYPE html>
|
||||
<html lang="en" ng-app>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>{% block title %}funkwhale_api{% endblock title %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
{% block css %}
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.rawgit.com/twbs/bootstrap/v4-dev/dist/css/bootstrap.css">
|
||||
|
||||
<!-- Your stuff: Third-party css libraries go here -->
|
||||
|
||||
<!-- This file store project specific CSS -->
|
||||
<link href="{% static 'css/project.css' %}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block angular %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="m-b">
|
||||
<nav class="navbar navbar-dark navbar-static-top bg-inverse">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">funkwhale_api</a>
|
||||
<button type="button" class="navbar-toggler hidden-sm-up pull-right" data-toggle="collapse" data-target="#bs-navbar-collapse-1">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Collect the nav links, forms, and other content for toggling -->
|
||||
<div class="collapse navbar-toggleable-xs" id="bs-navbar-collapse-1">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="">About</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav navbar-nav pull-right">
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'users:detail' request.user.username %}">{% trans "My Profile" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'account_logout' %}">{% trans "Logout" %}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a id="sign-up-link" class="nav-link" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a id="log-in-link" class="nav-link" href="{% url 'account_login' %}">{% trans "Log In" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
<p>Use this document as a way to quick start any new project.</p>
|
||||
{% endblock content %}
|
||||
|
||||
</div> <!-- /container -->
|
||||
|
||||
{% block modal %}{% endblock modal %}
|
||||
|
||||
<!-- Le javascript
|
||||
================================================== -->
|
||||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
{% block javascript %}
|
||||
<!-- Latest JQuery -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||
|
||||
<!-- Latest compiled and minified JavaScript -->
|
||||
<script src="https://cdn.rawgit.com/twbs/bootstrap/v4-dev/dist/js/bootstrap.js"></script>
|
||||
|
||||
<!-- Your stuff: Third-party javascript libraries go here -->
|
||||
|
||||
<!-- place project specific Javascript in this file -->
|
||||
<script src="{% static 'js/project.js' %}"></script>
|
||||
{% endblock javascript %}
|
||||
</body>
|
||||
</html>
|
|
@ -1 +0,0 @@
|
|||
{% extends "base.html" %}
|
|
@ -1 +0,0 @@
|
|||
{% extends "base.html" %}
|
|
@ -50,9 +50,6 @@ mutagen>=1.39,<1.40
|
|||
django-taggit>=0.22,<0.23
|
||||
# Until this is merged
|
||||
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
||||
# Until this is merged
|
||||
#django-cachalot==1.5.0
|
||||
git+https://github.com/EliotBerriot/django-cachalot.git@django-2
|
||||
|
||||
django-dynamic-preferences>=1.5,<1.6
|
||||
pyacoustid>=1.1.5,<1.2
|
||||
|
|
|
@ -9,7 +9,8 @@ from . import data as api_data
|
|||
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def test_set_acoustid_on_track_file(factories, mocker):
|
||||
def test_set_acoustid_on_track_file(factories, mocker, preferences):
|
||||
preferences['providers_acoustid__api_key'] = 'test'
|
||||
track_file = factories['music.TrackFile'](acoustid_track_id=None)
|
||||
id = 'e475bf79-c1ce-4441-bed7-1e33f226c0a2'
|
||||
payload = {
|
||||
|
@ -31,7 +32,7 @@ def test_set_acoustid_on_track_file(factories, mocker):
|
|||
|
||||
assert str(track_file.acoustid_track_id) == id
|
||||
assert r == id
|
||||
m.assert_called_once_with('', track_file.audio_file.path, parse=False)
|
||||
m.assert_called_once_with('test', track_file.audio_file.path, parse=False)
|
||||
|
||||
|
||||
def test_set_acoustid_on_track_file_required_high_score(factories, mocker):
|
||||
|
@ -48,7 +49,9 @@ def test_set_acoustid_on_track_file_required_high_score(factories, mocker):
|
|||
assert track_file.acoustid_track_id is None
|
||||
|
||||
|
||||
def test_import_job_can_run_with_file_and_acoustid(factories, mocker):
|
||||
def test_import_job_can_run_with_file_and_acoustid(
|
||||
preferences, factories, mocker):
|
||||
preferences['providers_acoustid__api_key'] = 'test'
|
||||
path = os.path.join(DATA_DIR, 'test.ogg')
|
||||
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
|
||||
acoustid_payload = {
|
||||
|
@ -88,7 +91,46 @@ def test_import_job_can_run_with_file_and_acoustid(factories, mocker):
|
|||
assert job.source == 'file://'
|
||||
|
||||
|
||||
def test_import_job_can_be_skipped(factories, mocker):
|
||||
def test_run_import_skipping_accoustid(factories, mocker):
|
||||
m = mocker.patch('funkwhale_api.music.tasks._do_import')
|
||||
path = os.path.join(DATA_DIR, 'test.ogg')
|
||||
job = factories['music.FileImportJob'](audio_file__path=path)
|
||||
tasks.import_job_run(import_job_id=job.pk, use_acoustid=False)
|
||||
m.assert_called_once_with(job, False, use_acoustid=False)
|
||||
|
||||
|
||||
def test__do_import_skipping_accoustid(factories, mocker):
|
||||
t = factories['music.Track']()
|
||||
m = mocker.patch(
|
||||
'funkwhale_api.music.tasks.import_track_data_from_path',
|
||||
return_value=t)
|
||||
path = os.path.join(DATA_DIR, 'test.ogg')
|
||||
job = factories['music.FileImportJob'](
|
||||
mbid=None,
|
||||
audio_file__path=path)
|
||||
p = job.audio_file.path
|
||||
tasks._do_import(job, replace=False, use_acoustid=False)
|
||||
m.assert_called_once_with(p)
|
||||
|
||||
|
||||
def test__do_import_skipping_accoustid_if_no_key(
|
||||
factories, mocker, preferences):
|
||||
preferences['providers_acoustid__api_key'] = ''
|
||||
t = factories['music.Track']()
|
||||
m = mocker.patch(
|
||||
'funkwhale_api.music.tasks.import_track_data_from_path',
|
||||
return_value=t)
|
||||
path = os.path.join(DATA_DIR, 'test.ogg')
|
||||
job = factories['music.FileImportJob'](
|
||||
mbid=None,
|
||||
audio_file__path=path)
|
||||
p = job.audio_file.path
|
||||
tasks._do_import(job, replace=False, use_acoustid=False)
|
||||
m.assert_called_once_with(p)
|
||||
|
||||
|
||||
def test_import_job_can_be_skipped(factories, mocker, preferences):
|
||||
preferences['providers_acoustid__api_key'] = 'test'
|
||||
path = os.path.join(DATA_DIR, 'test.ogg')
|
||||
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
|
||||
track_file = factories['music.TrackFile'](track__mbid=mbid)
|
||||
|
@ -124,7 +166,8 @@ def test_import_job_can_be_skipped(factories, mocker):
|
|||
assert job.status == 'skipped'
|
||||
|
||||
|
||||
def test_import_job_can_be_errored(factories, mocker):
|
||||
def test_import_job_can_be_errored(factories, mocker, preferences):
|
||||
preferences['providers_acoustid__api_key'] = 'test'
|
||||
path = os.path.join(DATA_DIR, 'test.ogg')
|
||||
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
|
||||
track_file = factories['music.TrackFile'](track__mbid=mbid)
|
||||
|
|
|
@ -54,7 +54,7 @@ def test_management_command_requires_a_valid_username(factories, mocker):
|
|||
|
||||
|
||||
def test_import_files_creates_a_batch_and_job(factories, mocker):
|
||||
m = m = mocker.patch('funkwhale_api.common.utils.on_commit')
|
||||
m = mocker.patch('funkwhale_api.common.utils.on_commit')
|
||||
user = factories['users.User'](username='me')
|
||||
path = os.path.join(DATA_DIR, 'dummy_file.ogg')
|
||||
call_command(
|
||||
|
@ -77,4 +77,24 @@ def test_import_files_creates_a_batch_and_job(factories, mocker):
|
|||
assert job.source == 'file://' + path
|
||||
m.assert_called_once_with(
|
||||
music_tasks.import_job_run.delay,
|
||||
import_job_id=job.pk)
|
||||
import_job_id=job.pk,
|
||||
use_acoustid=True)
|
||||
|
||||
|
||||
def test_import_files_skip_acoustid(factories, mocker):
|
||||
m = mocker.patch('funkwhale_api.common.utils.on_commit')
|
||||
user = factories['users.User'](username='me')
|
||||
path = os.path.join(DATA_DIR, 'dummy_file.ogg')
|
||||
call_command(
|
||||
'import_files',
|
||||
path,
|
||||
username='me',
|
||||
async=True,
|
||||
no_acoustid=True,
|
||||
interactive=False)
|
||||
batch = user.imports.latest('id')
|
||||
job = batch.jobs.first()
|
||||
m.assert_called_once_with(
|
||||
music_tasks.import_job_run.delay,
|
||||
import_job_id=job.pk,
|
||||
use_acoustid=False)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash -eux
|
||||
version="develop"
|
||||
music_path="/usr/share/music"
|
||||
demo_path="/srv/funkwhale-demo/demo"
|
||||
|
||||
echo 'Cleaning everything...'
|
||||
cd $demo_path
|
||||
docker-compose down -v || echo 'Nothing to stop'
|
||||
rm -rf /srv/funkwhale-demo/demo/*
|
||||
mkdir -p $demo_path
|
||||
echo 'Downloading demo files...'
|
||||
curl -L -o docker-compose.yml "https://code.eliotberriot.com/funkwhale/funkwhale/raw/$version/deploy/docker-compose.yml"
|
||||
curl -L -o .env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/$version/deploy/env.prod.sample"
|
||||
|
||||
mkdir data/
|
||||
cp -r $music_path data/music
|
||||
|
||||
curl -L -o front.zip "https://code.eliotberriot.com/funkwhale/funkwhale/-/jobs/artifacts/$version/download?job=build_front"
|
||||
unzip front.zip
|
||||
|
||||
echo "FUNKWHALE_URL=https://demo.funkwhale.audio/" >> .env
|
||||
echo "DJANGO_SECRET_KEY=demo" >> .env
|
||||
echo "DJANGO_ALLOWED_HOSTS=demo.funkwhale.audio" >> .env
|
||||
echo "FUNKWHALE_VERSION=$version" >> .env
|
||||
echo "FUNKWHALE_API_PORT=5001" >> .env
|
||||
|
||||
docker-compose pull
|
||||
docker-compose up -d postgres redis
|
||||
sleep 5
|
||||
docker-compose run --rm api demo/load-demo-data.sh
|
||||
docker-compose up -d
|
|
@ -20,7 +20,7 @@ services:
|
|||
restart: unless-stopped
|
||||
image: funkwhale/funkwhale:${FUNKWHALE_VERSION:-latest}
|
||||
env_file: .env
|
||||
command: python manage.py celery worker
|
||||
command: celery -A funkwhale_api.taskapp worker -l INFO
|
||||
links:
|
||||
- postgres
|
||||
- redis
|
||||
|
|
|
@ -31,7 +31,7 @@ FUNKWHALE_API_PORT=5000
|
|||
|
||||
# Replace this by the definitive, public domain you will use for
|
||||
# your instance
|
||||
FUNKWHALE_URL=https.//yourdomain.funwhale
|
||||
FUNKWHALE_URL=https://yourdomain.funwhale
|
||||
|
||||
# API/Django configuration
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ User=funkwhale
|
|||
# adapt this depending on the path of your funkwhale installation
|
||||
WorkingDirectory=/srv/funkwhale/api
|
||||
EnvironmentFile=/srv/funkwhale/config/.env
|
||||
ExecStart=/usr/local/bin/daphne -b ${FUNKWHALE_API_IP} -p ${FUNKWHALE_API_PORT} config.asgi:application
|
||||
ExecStart=/srv/funkwhale/virtualenv/bin/daphne -b ${FUNKWHALE_API_IP} -p ${FUNKWHALE_API_PORT} config.asgi:application
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
@ -8,7 +8,7 @@ User=funkwhale
|
|||
# adapt this depending on the path of your funkwhale installation
|
||||
WorkingDirectory=/srv/funkwhale/api
|
||||
EnvironmentFile=/srv/funkwhale/config/.env
|
||||
ExecStart=/srv/funkwhale/virtualenv/bin/python manage.py celery worker
|
||||
ExecStart=/srv/funkwhale/virtualenv/bin/celery -A funkwhale_api.taskapp worker -l INFO
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# global proxy conf
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
|
||||
# websocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
|
@ -48,20 +48,6 @@ server {
|
|||
|
||||
root /srv/funkwhale/front/dist;
|
||||
|
||||
# global proxy conf
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
|
||||
# websocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ @rewrites;
|
||||
}
|
||||
|
@ -70,6 +56,7 @@ server {
|
|||
rewrite ^(.+)$ /index.html last;
|
||||
}
|
||||
location /api/ {
|
||||
include /etc/nginx/funkwhale_proxy.conf;
|
||||
# this is needed if you have file import via upload enabled
|
||||
client_max_body_size 30M;
|
||||
proxy_pass http://funkwhale-api/api/;
|
||||
|
@ -89,6 +76,7 @@ server {
|
|||
|
||||
# Transcoding logic and caching
|
||||
location = /transcode-auth {
|
||||
include /etc/nginx/funkwhale_proxy.conf;
|
||||
# needed so we can authenticate transcode requests, but still
|
||||
# cache the result
|
||||
internal;
|
||||
|
@ -97,14 +85,13 @@ server {
|
|||
if ($request_uri ~* "[^\?]+\?(.*)$") {
|
||||
set $query $1;
|
||||
}
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_pass http://funkwhale-api/api/v1/trackfiles/viewable/?$query;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
}
|
||||
|
||||
location /api/v1/trackfiles/transcode/ {
|
||||
include /etc/nginx/funkwhale_proxy.conf;
|
||||
# this block deals with authenticating and caching transcoding
|
||||
# requests. Caching is heavily recommended as transcoding
|
||||
# is a CPU intensive process.
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
Instance configuration
|
||||
======================
|
||||
|
||||
General configuration is achieved using two type of settings.
|
||||
|
||||
Environment variables
|
||||
---------------------
|
||||
|
||||
Those are located in your ``.env`` file, which you should have created
|
||||
during installation.
|
||||
|
||||
Options from this file are heavily commented, and usually target lower level
|
||||
and technical aspects of your instance, such as database credentials.
|
||||
|
||||
.. note::
|
||||
|
||||
You should restart all funwhale processes when you change the values
|
||||
on environment variables.
|
||||
|
||||
|
||||
Instance settings
|
||||
-----------------
|
||||
|
||||
Those settings are stored in database and do not require a restart of your
|
||||
instance after modification. They typically relate to higher level configuration,
|
||||
such your instance description, signup policy and so on.
|
||||
|
||||
There is no polished interface for those settings, yet, but you can view update
|
||||
them using the administration interface provided by Django (the framework funkwhale is built on).
|
||||
|
||||
The URL should be ``/api/admin/dynamic_preferences/globalpreferencemodel/`` (prepend your domain in front of it, of course).
|
||||
|
||||
If you plan to use acoustid and external imports
|
||||
(e.g. with the youtube backends), you should edit the corresponding
|
||||
settings in this interface.
|
|
@ -25,6 +25,10 @@ to the ``/music`` directory on the container:
|
|||
For the best results, we recommand tagging your music collection through
|
||||
`Picard <http://picard.musicbrainz.org/>`_ in order to have the best quality metadata.
|
||||
|
||||
.. note::
|
||||
|
||||
Autotagging using acoustid is experimental now and can yield unexpected
|
||||
result. You can disable acoustid by passing the --no-acoustid flag.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ Funkwhale is a self-hosted, modern free and open-source music server, heavily in
|
|||
|
||||
features
|
||||
installation/index
|
||||
configuration
|
||||
importing-music
|
||||
changelog
|
||||
|
||||
|
|
|
@ -43,6 +43,15 @@ you should now be able to open a postgresql shell:
|
|||
|
||||
sudo -u funkwhale -H psql
|
||||
|
||||
Unless you give a superuser access to the database user, you should also
|
||||
enable some extensions on your database server, as those are required
|
||||
for funkwhale to work properly:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo -u postgres psql -c 'CREATE EXTENSION "unaccent";''
|
||||
|
||||
|
||||
Cache setup (Redis)
|
||||
-------------------
|
||||
|
||||
|
|
|
@ -59,10 +59,11 @@ Ensure you have a recent version of nginx on your server. On debian-like system,
|
|||
apt-get update
|
||||
apt-get install nginx
|
||||
|
||||
Then, download our sample virtualhost file:
|
||||
Then, download our sample virtualhost file and proxy conf:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
curl -L -o /etc/nginx/funkwhale_proxy.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale_proxy.conf"
|
||||
curl -L -o /etc/nginx/sites-enabled/funkwhale.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/nginx.conf"
|
||||
|
||||
Ensure static assets and proxy pass match your configuration, and check the configuration is valid with ``nginx -t``. If everything is fine, you can restart your nginx server with ``service nginx restart``.
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
<template>
|
||||
<div class="ui pagination borderless menu">
|
||||
<a
|
||||
@click="selectPage(1)"
|
||||
:class="[{'disabled': current === 1}, 'item']"><i class="angle double left icon"></i></a>
|
||||
<a
|
||||
@click="selectPage(current - 1)"
|
||||
:class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a>
|
||||
<a
|
||||
v-for="page in pages"
|
||||
@click="selectPage(page)"
|
||||
:class="[{'active': page === current}, 'item']">
|
||||
{{ page }}
|
||||
</a>
|
||||
<template>
|
||||
<a
|
||||
v-if="page !== 'skip'"
|
||||
v-for="page in pages"
|
||||
@click="selectPage(page)"
|
||||
:class="[{'active': page === current}, 'item']">
|
||||
{{ page }}
|
||||
</a>
|
||||
<a v-else class="disabled item">
|
||||
...
|
||||
</a>
|
||||
</template>
|
||||
<a
|
||||
@click="selectPage(current + 1)"
|
||||
:class="[{'disabled': current + 1 > maxPage}, 'item']"><i class="angle right icon"></i></a>
|
||||
<a
|
||||
@click="selectPage(maxPage)"
|
||||
:class="[{'disabled': current === maxPage}, 'item']"><i class="angle double right icon"></i></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -32,7 +32,38 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
pages: function () {
|
||||
return _.range(1, this.maxPage + 1)
|
||||
let range = 2
|
||||
let current = this.current
|
||||
let beginning = _.range(1, Math.min(this.maxPage, 1 + range))
|
||||
let middle = _.range(Math.max(1, current - range + 1), Math.min(this.maxPage, current + range))
|
||||
let end = _.range(this.maxPage, Math.max(1, this.maxPage - range))
|
||||
let allowed = beginning.concat(middle, end)
|
||||
allowed = _.uniq(allowed)
|
||||
allowed = _.sortBy(allowed, [(e) => { return e }])
|
||||
let final = []
|
||||
allowed.forEach(p => {
|
||||
let last = final.slice(-1)[0]
|
||||
let consecutive = true
|
||||
if (last === 'skip') {
|
||||
consecutive = false
|
||||
} else {
|
||||
if (!last) {
|
||||
consecutive = true
|
||||
} else {
|
||||
consecutive = last + 1 === p
|
||||
}
|
||||
}
|
||||
if (consecutive) {
|
||||
final.push(p)
|
||||
} else {
|
||||
if (p !== 'skip') {
|
||||
final.push('skip')
|
||||
final.push(p)
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log(final)
|
||||
return final
|
||||
},
|
||||
maxPage: function () {
|
||||
return Math.ceil(this.total / this.paginateBy)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<div :class="['ui', {'tiny': discrete}, 'buttons']">
|
||||
<button title="Add to current queue" @click="add" :class="['ui', {'mini': discrete}, {disabled: playableTracks.length === 0}, 'button']">
|
||||
<button
|
||||
title="Add to current queue"
|
||||
@click="add"
|
||||
:class="['ui', {loading: isLoading}, {'mini': discrete}, {disabled: playableTracks.length === 0}, 'button']">
|
||||
<i class="ui play icon"></i>
|
||||
<template v-if="!discrete"><slot>Play</slot></template>
|
||||
</button>
|
||||
|
@ -26,6 +29,11 @@ export default {
|
|||
track: {type: Object, required: false},
|
||||
discrete: {type: Boolean, default: false}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (!this.track & !this.tracks) {
|
||||
logger.default.error('You have to provide either a track or tracks property')
|
||||
|
@ -50,10 +58,19 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
triggerLoad () {
|
||||
let self = this
|
||||
this.isLoading = true
|
||||
setTimeout(() => {
|
||||
self.isLoading = false
|
||||
}, 500)
|
||||
},
|
||||
add () {
|
||||
this.triggerLoad()
|
||||
this.$store.dispatch('queue/appendMany', {tracks: this.playableTracks})
|
||||
},
|
||||
addNext (next) {
|
||||
this.triggerLoad()
|
||||
this.$store.dispatch('queue/appendMany', {tracks: this.playableTracks, index: this.$store.state.queue.currentIndex + 1})
|
||||
if (next) {
|
||||
this.$store.dispatch('queue/next')
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 957 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Loading…
Reference in New Issue