Merge branch 'release/0.6.1'

This commit is contained in:
Eliot Berriot 2018-03-06 21:56:11 +01:00
commit 6bf73384d2
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
40 changed files with 28019 additions and 27381 deletions

View File

@ -1,4 +1,3 @@
API_AUTHENTICATION_REQUIRED=True
CACHALOT_ENABLED=False
RAVEN_ENABLED=false
RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5

2
.gitignore vendored
View File

@ -84,3 +84,5 @@ front/test/unit/coverage
front/test/e2e/reports
front/selenium-debug.log
docs/_build
data/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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('.')])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">
&#9776;
</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>

View File

@ -1 +0,0 @@
{% extends "base.html" %}

View File

@ -1 +0,0 @@
{% extends "base.html" %}

View File

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

View File

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

View File

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

31
demo/setup.sh Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

35
docs/configuration.rst Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 957 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB