Merge branch 'release/0.2.4'
This commit is contained in:
commit
142a8050ee
|
@ -18,6 +18,8 @@ test_api:
|
||||||
- pip install -r requirements/test.txt
|
- pip install -r requirements/test.txt
|
||||||
script:
|
script:
|
||||||
- pytest
|
- pytest
|
||||||
|
variables:
|
||||||
|
DATABASE_URL: "sqlite://"
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
|
||||||
|
0.2.5 (unreleased)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
|
||||||
|
0.2.4 (2017-12-14)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- Models: now store relese group mbid on Album model (#7)
|
||||||
|
- Models: now bind import job to track files (#44)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Library: fixen broken "play all albums" button on artist cards in Artist browsing view (#43)
|
|
@ -4,7 +4,7 @@ set -e
|
||||||
# Since docker-compose relies heavily on environment variables itself for configuration, we'd have to define multiple
|
# 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
|
# environment variables just to support cookiecutter out of the box. That makes no sense, so this little entrypoint
|
||||||
# does all this for us.
|
# does all this for us.
|
||||||
export REDIS_URL=redis://redis:6379/0
|
export CACHE_URL=redis://redis:6379/0
|
||||||
|
|
||||||
# the official postgres image uses 'postgres' as default user if not set explictly.
|
# the official postgres image uses 'postgres' as default user if not set explictly.
|
||||||
if [ -z "$POSTGRES_ENV_POSTGRES_USER" ]; then
|
if [ -z "$POSTGRES_ENV_POSTGRES_USER" ]; then
|
||||||
|
@ -13,7 +13,7 @@ fi
|
||||||
|
|
||||||
export DATABASE_URL=postgres://$POSTGRES_ENV_POSTGRES_USER:$POSTGRES_ENV_POSTGRES_PASSWORD@postgres:5432/$POSTGRES_ENV_POSTGRES_USER
|
export DATABASE_URL=postgres://$POSTGRES_ENV_POSTGRES_USER:$POSTGRES_ENV_POSTGRES_PASSWORD@postgres:5432/$POSTGRES_ENV_POSTGRES_USER
|
||||||
|
|
||||||
export CELERY_BROKER_URL=$REDIS_URL
|
export CELERY_BROKER_URL=$CACHE_URL
|
||||||
|
|
||||||
# we copy the frontend files, if any so we can serve them from the outside
|
# we copy the frontend files, if any so we can serve them from the outside
|
||||||
if [ -d "frontend" ]; then
|
if [ -d "frontend" ]; then
|
||||||
|
|
|
@ -124,7 +124,7 @@ MANAGERS = ADMINS
|
||||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
|
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
|
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
|
||||||
'default': env.db("DATABASE_URL", default="postgresql://postgres@postgres/postgres"),
|
'default': env.db("DATABASE_URL"),
|
||||||
}
|
}
|
||||||
DATABASES['default']['ATOMIC_REQUESTS'] = True
|
DATABASES['default']['ATOMIC_REQUESTS'] = True
|
||||||
#
|
#
|
||||||
|
@ -199,7 +199,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap3'
|
||||||
# STATIC FILE CONFIGURATION
|
# STATIC FILE CONFIGURATION
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
|
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
|
||||||
STATIC_ROOT = str(ROOT_DIR('staticfiles'))
|
STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR('staticfiles')))
|
||||||
|
|
||||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
|
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
|
||||||
STATIC_URL = env("STATIC_URL", default='/staticfiles/')
|
STATIC_URL = env("STATIC_URL", default='/staticfiles/')
|
||||||
|
@ -218,12 +218,10 @@ STATICFILES_FINDERS = (
|
||||||
# MEDIA CONFIGURATION
|
# MEDIA CONFIGURATION
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
||||||
MEDIA_ROOT = str(APPS_DIR('media'))
|
MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR('media')))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = env("MEDIA_URL", default='/media/')
|
||||||
|
|
||||||
# URL Configuration
|
# URL Configuration
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
@ -253,26 +251,24 @@ LOGIN_URL = 'account_login'
|
||||||
# SLUGLIFIER
|
# SLUGLIFIER
|
||||||
AUTOSLUG_SLUGIFY_FUNCTION = 'slugify.slugify'
|
AUTOSLUG_SLUGIFY_FUNCTION = 'slugify.slugify'
|
||||||
|
|
||||||
########## CELERY
|
CACHE_DEFAULT = "redis://127.0.0.1:6379/0"
|
||||||
INSTALLED_APPS += ('funkwhale_api.taskapp.celery.CeleryConfig',)
|
|
||||||
# if you are not using the django database broker (e.g. rabbitmq, redis, memcached), you can remove the next line.
|
|
||||||
INSTALLED_APPS += ('kombu.transport.django',)
|
|
||||||
BROKER_URL = env("CELERY_BROKER_URL", default='django://')
|
|
||||||
########## END CELERY
|
|
||||||
|
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": env.cache_url('CACHE_URL', default=CACHE_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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CACHES["default"]["BACKEND"] = "django_redis.cache.RedisCache"
|
||||||
|
CACHES["default"]["OPTIONS"] = {
|
||||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
|
||||||
|
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
########## CELERY
|
||||||
|
INSTALLED_APPS += ('funkwhale_api.taskapp.celery.CeleryConfig',)
|
||||||
|
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' %}
|
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
|
||||||
ADMIN_URL = r'^admin/'
|
ADMIN_URL = r'^admin/'
|
||||||
# Your common stuff: Below this line define 3rd party library settings
|
# Your common stuff: Below this line define 3rd party library settings
|
||||||
|
@ -336,3 +332,8 @@ MUSICBRAINZ_CACHE_DURATION = env.int(
|
||||||
)
|
)
|
||||||
|
|
||||||
CACHALOT_ENABLED = env.bool('CACHALOT_ENABLED', default=True)
|
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
|
||||||
|
|
|
@ -54,7 +54,7 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Hosts/domain names that are valid for this site
|
# Hosts/domain names that are valid for this site
|
||||||
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
|
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['funkwhale.io'])
|
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
|
||||||
# END SITE CONFIGURATION
|
# END SITE CONFIGURATION
|
||||||
|
|
||||||
INSTALLED_APPS += ("gunicorn", )
|
INSTALLED_APPS += ("gunicorn", )
|
||||||
|
@ -65,10 +65,6 @@ INSTALLED_APPS += ("gunicorn", )
|
||||||
# ------------------------
|
# ------------------------
|
||||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||||
|
|
||||||
# URL that handles the media served from MEDIA_ROOT, used for managing
|
|
||||||
# stored files.
|
|
||||||
MEDIA_URL = '/media/'
|
|
||||||
|
|
||||||
# Static Assets
|
# Static Assets
|
||||||
# ------------------------
|
# ------------------------
|
||||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
|
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
|
||||||
|
@ -92,11 +88,6 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [
|
||||||
'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ]),
|
'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ]),
|
||||||
]
|
]
|
||||||
|
|
||||||
# DATABASE CONFIGURATION
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
|
|
||||||
DATABASES['default'] = env.db("DATABASE_URL")
|
|
||||||
|
|
||||||
# CACHING
|
# CACHING
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Heroku URL does not pass the DB number, so we parse it in
|
# Heroku URL does not pass the DB number, so we parse it in
|
||||||
|
@ -151,7 +142,5 @@ LOGGING = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Custom Admin URL, use {% url 'admin:index' %}
|
|
||||||
ADMIN_URL = env('DJANGO_ADMIN_URL')
|
|
||||||
|
|
||||||
# Your production stuff: Below this line define 3rd party library settings
|
# Your production stuff: Below this line define 3rd party library settings
|
||||||
|
|
|
@ -22,6 +22,9 @@ CACHES = {
|
||||||
'LOCATION': ''
|
'LOCATION': ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
INSTALLED_APPS += ('kombu.transport.django',)
|
||||||
|
BROKER_URL = 'django://'
|
||||||
|
|
||||||
# TESTING
|
# TESTING
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
|
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
__version__ = '0.2.3'
|
__version__ = '0.2.4'
|
||||||
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-12-14 22:05
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.sites.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sites', '0002_set_site_domain_and_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='site',
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.sites.models.SiteManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='domain',
|
||||||
|
field=models.CharField(max_length=100, unique=True, validators=[django.contrib.sites.models._simple_domain_name_validator], verbose_name='domain name'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,30 +2,35 @@ from django.contrib import admin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Artist)
|
@admin.register(models.Artist)
|
||||||
class ArtistAdmin(admin.ModelAdmin):
|
class ArtistAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'mbid', 'creation_date']
|
list_display = ['name', 'mbid', 'creation_date']
|
||||||
search_fields = ['name', 'mbid']
|
search_fields = ['name', 'mbid']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Album)
|
@admin.register(models.Album)
|
||||||
class AlbumAdmin(admin.ModelAdmin):
|
class AlbumAdmin(admin.ModelAdmin):
|
||||||
list_display = ['title', 'artist', 'mbid', 'release_date', 'creation_date']
|
list_display = ['title', 'artist', 'mbid', 'release_date', 'creation_date']
|
||||||
search_fields = ['title', 'artist__name', 'mbid']
|
search_fields = ['title', 'artist__name', 'mbid']
|
||||||
list_select_related = True
|
list_select_related = True
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Track)
|
@admin.register(models.Track)
|
||||||
class TrackAdmin(admin.ModelAdmin):
|
class TrackAdmin(admin.ModelAdmin):
|
||||||
list_display = ['title', 'artist', 'album', 'mbid']
|
list_display = ['title', 'artist', 'album', 'mbid']
|
||||||
search_fields = ['title', 'artist__name', 'album__title', 'mbid']
|
search_fields = ['title', 'artist__name', 'album__title', 'mbid']
|
||||||
list_select_related = True
|
list_select_related = True
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.ImportBatch)
|
@admin.register(models.ImportBatch)
|
||||||
class ImportBatchAdmin(admin.ModelAdmin):
|
class ImportBatchAdmin(admin.ModelAdmin):
|
||||||
list_display = ['creation_date', 'status']
|
list_display = ['creation_date', 'status']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.ImportJob)
|
@admin.register(models.ImportJob)
|
||||||
class ImportJobAdmin(admin.ModelAdmin):
|
class ImportJobAdmin(admin.ModelAdmin):
|
||||||
list_display = ['source', 'batch', 'status', 'mbid']
|
list_display = ['source', 'batch', 'track_file', 'status', 'mbid']
|
||||||
list_select_related = True
|
list_select_related = True
|
||||||
search_fields = ['source', 'batch__pk', 'mbid']
|
search_fields = ['source', 'batch__pk', 'mbid']
|
||||||
list_filter = ['status']
|
list_filter = ['status']
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-12-13 22:11
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('music', '0012_auto_20161122_1905'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='importjob',
|
||||||
|
options={'ordering': ('id',)},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='track',
|
||||||
|
options={'ordering': ['album', 'position']},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='album',
|
||||||
|
name='release_group_id',
|
||||||
|
field=models.UUIDField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-12-14 21:14
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('music', '0013_auto_20171213_2211'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='importjob',
|
||||||
|
name='track_file',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='music.TrackFile'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from funkwhale_api.common.utils import rename_file
|
||||||
|
|
||||||
|
|
||||||
|
def bind_jobs(apps, schema_editor):
|
||||||
|
TrackFile = apps.get_model("music", "TrackFile")
|
||||||
|
ImportJob = apps.get_model("music", "ImportJob")
|
||||||
|
|
||||||
|
for job in ImportJob.objects.all().only('mbid'):
|
||||||
|
f = TrackFile.objects.filter(track__mbid=job.mbid).first()
|
||||||
|
if not f:
|
||||||
|
print('No file for mbid {}'.format(job.mbid))
|
||||||
|
continue
|
||||||
|
job.track_file = f
|
||||||
|
job.save(update_fields=['track_file'])
|
||||||
|
|
||||||
|
|
||||||
|
def rewind(apps, schema_editor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('music', '0014_importjob_track_file'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(bind_jobs, rewind),
|
||||||
|
]
|
|
@ -110,13 +110,14 @@ class Album(APIModelMixin):
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
artist = models.ForeignKey(Artist, related_name='albums')
|
artist = models.ForeignKey(Artist, related_name='albums')
|
||||||
release_date = models.DateField(null=True)
|
release_date = models.DateField(null=True)
|
||||||
|
release_group_id = models.UUIDField(null=True, blank=True)
|
||||||
cover = VersatileImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True)
|
cover = VersatileImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True)
|
||||||
TYPE_CHOICES = (
|
TYPE_CHOICES = (
|
||||||
('album', 'Album'),
|
('album', 'Album'),
|
||||||
)
|
)
|
||||||
type = models.CharField(choices=TYPE_CHOICES, max_length=30, default='album')
|
type = models.CharField(choices=TYPE_CHOICES, max_length=30, default='album')
|
||||||
|
|
||||||
api_includes = ['artist-credits', 'recordings', 'media']
|
api_includes = ['artist-credits', 'recordings', 'media', 'release-groups']
|
||||||
api = musicbrainz.api.releases
|
api = musicbrainz.api.releases
|
||||||
musicbrainz_model = 'release'
|
musicbrainz_model = 'release'
|
||||||
musicbrainz_mapping = {
|
musicbrainz_mapping = {
|
||||||
|
@ -127,6 +128,10 @@ class Album(APIModelMixin):
|
||||||
'musicbrainz_field_name': 'release-list',
|
'musicbrainz_field_name': 'release-list',
|
||||||
'converter': lambda v: int(v[0]['medium-list'][0]['position']),
|
'converter': lambda v: int(v[0]['medium-list'][0]['position']),
|
||||||
},
|
},
|
||||||
|
'release_group_id': {
|
||||||
|
'musicbrainz_field_name': 'release-group',
|
||||||
|
'converter': lambda v: v['id'],
|
||||||
|
},
|
||||||
'title': {
|
'title': {
|
||||||
'musicbrainz_field_name': 'title',
|
'musicbrainz_field_name': 'title',
|
||||||
},
|
},
|
||||||
|
@ -388,6 +393,8 @@ class ImportBatch(models.Model):
|
||||||
|
|
||||||
class ImportJob(models.Model):
|
class ImportJob(models.Model):
|
||||||
batch = models.ForeignKey(ImportBatch, related_name='jobs')
|
batch = models.ForeignKey(ImportBatch, related_name='jobs')
|
||||||
|
track_file = models.ForeignKey(
|
||||||
|
TrackFile, related_name='jobs', null=True, blank=True)
|
||||||
source = models.URLField()
|
source = models.URLField()
|
||||||
mbid = models.UUIDField(editable=False)
|
mbid = models.UUIDField(editable=False)
|
||||||
STATUS_CHOICES = (
|
STATUS_CHOICES = (
|
||||||
|
@ -408,10 +415,12 @@ class ImportJob(models.Model):
|
||||||
elif track.files.count() > 0:
|
elif track.files.count() > 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
track_file = track_file or TrackFile(track=track, source=self.source)
|
track_file = track_file or TrackFile(
|
||||||
|
track=track, source=self.source)
|
||||||
track_file.download_file()
|
track_file.download_file()
|
||||||
track_file.save()
|
track_file.save()
|
||||||
self.status = 'finished'
|
self.status = 'finished'
|
||||||
|
self.track_file = track_file
|
||||||
self.save()
|
self.save()
|
||||||
return track.pk
|
return track.pk
|
||||||
|
|
||||||
|
|
|
@ -9,35 +9,26 @@ class TagSerializer(serializers.ModelSerializer):
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ('id', 'name', 'slug')
|
fields = ('id', 'name', 'slug')
|
||||||
|
|
||||||
|
|
||||||
class SimpleArtistSerializer(serializers.ModelSerializer):
|
class SimpleArtistSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Artist
|
model = models.Artist
|
||||||
fields = ('id', 'mbid', 'name')
|
fields = ('id', 'mbid', 'name')
|
||||||
|
|
||||||
|
|
||||||
class ArtistSerializer(serializers.ModelSerializer):
|
class ArtistSerializer(serializers.ModelSerializer):
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
tags = TagSerializer(many=True, read_only=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Artist
|
model = models.Artist
|
||||||
fields = ('id', 'mbid', 'name', 'tags')
|
fields = ('id', 'mbid', 'name', 'tags')
|
||||||
|
|
||||||
class ImportJobSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.ImportJob
|
|
||||||
fields = ('id', 'mbid', 'source', 'status')
|
|
||||||
|
|
||||||
class ImportBatchSerializer(serializers.ModelSerializer):
|
|
||||||
jobs = ImportJobSerializer(many=True, read_only=True)
|
|
||||||
class Meta:
|
|
||||||
model = models.ImportBatch
|
|
||||||
fields = ('id', 'jobs', 'status', 'creation_date')
|
|
||||||
|
|
||||||
|
|
||||||
class TrackFileSerializer(serializers.ModelSerializer):
|
class TrackFileSerializer(serializers.ModelSerializer):
|
||||||
path = serializers.SerializerMethodField()
|
path = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.TrackFile
|
model = models.TrackFile
|
||||||
fields = ('id', 'path', 'duration', 'source', 'filename')
|
fields = ('id', 'path', 'duration', 'source', 'filename', 'track')
|
||||||
|
|
||||||
def get_path(self, o):
|
def get_path(self, o):
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
|
@ -46,12 +37,14 @@ class TrackFileSerializer(serializers.ModelSerializer):
|
||||||
url = request.build_absolute_uri(url)
|
url = request.build_absolute_uri(url)
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
class SimpleAlbumSerializer(serializers.ModelSerializer):
|
class SimpleAlbumSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
fields = ('id', 'mbid', 'title', 'release_date', 'cover')
|
fields = ('id', 'mbid', 'title', 'release_date', 'cover')
|
||||||
|
|
||||||
|
|
||||||
class AlbumSerializer(serializers.ModelSerializer):
|
class AlbumSerializer(serializers.ModelSerializer):
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
tags = TagSerializer(many=True, read_only=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -81,6 +74,7 @@ class TrackSerializer(LyricsMixin):
|
||||||
'position',
|
'position',
|
||||||
'lyrics')
|
'lyrics')
|
||||||
|
|
||||||
|
|
||||||
class TrackSerializerNested(LyricsMixin):
|
class TrackSerializerNested(LyricsMixin):
|
||||||
artist = ArtistSerializer()
|
artist = ArtistSerializer()
|
||||||
files = TrackFileSerializer(many=True, read_only=True)
|
files = TrackFileSerializer(many=True, read_only=True)
|
||||||
|
@ -90,6 +84,7 @@ class TrackSerializerNested(LyricsMixin):
|
||||||
model = models.Track
|
model = models.Track
|
||||||
fields = ('id', 'mbid', 'title', 'artist', 'files', 'album', 'tags', 'lyrics')
|
fields = ('id', 'mbid', 'title', 'artist', 'files', 'album', 'tags', 'lyrics')
|
||||||
|
|
||||||
|
|
||||||
class AlbumSerializerNested(serializers.ModelSerializer):
|
class AlbumSerializerNested(serializers.ModelSerializer):
|
||||||
tracks = TrackSerializer(many=True, read_only=True)
|
tracks = TrackSerializer(many=True, read_only=True)
|
||||||
artist = SimpleArtistSerializer()
|
artist = SimpleArtistSerializer()
|
||||||
|
@ -99,6 +94,7 @@ class AlbumSerializerNested(serializers.ModelSerializer):
|
||||||
model = models.Album
|
model = models.Album
|
||||||
fields = ('id', 'mbid', 'title', 'cover', 'artist', 'release_date', 'tracks', 'tags')
|
fields = ('id', 'mbid', 'title', 'cover', 'artist', 'release_date', 'tracks', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class ArtistSerializerNested(serializers.ModelSerializer):
|
class ArtistSerializerNested(serializers.ModelSerializer):
|
||||||
albums = AlbumSerializerNested(many=True, read_only=True)
|
albums = AlbumSerializerNested(many=True, read_only=True)
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
tags = TagSerializer(many=True, read_only=True)
|
||||||
|
@ -111,3 +107,17 @@ class LyricsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Lyrics
|
model = models.Lyrics
|
||||||
fields = ('id', 'work', 'content', 'content_rendered')
|
fields = ('id', 'work', 'content', 'content_rendered')
|
||||||
|
|
||||||
|
|
||||||
|
class ImportJobSerializer(serializers.ModelSerializer):
|
||||||
|
track_file = TrackFileSerializer(read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = models.ImportJob
|
||||||
|
fields = ('id', 'mbid', 'source', 'status', 'track_file')
|
||||||
|
|
||||||
|
|
||||||
|
class ImportBatchSerializer(serializers.ModelSerializer):
|
||||||
|
jobs = ImportJobSerializer(many=True, read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = models.ImportBatch
|
||||||
|
fields = ('id', 'jobs', 'status', 'creation_date')
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import factory
|
import factory
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from funkwhale_api.users.tests.factories import UserFactory
|
||||||
|
|
||||||
SAMPLES_PATH = os.path.dirname(os.path.abspath(__file__))
|
SAMPLES_PATH = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +20,7 @@ class AlbumFactory(factory.django.DjangoModelFactory):
|
||||||
release_date = factory.Faker('date')
|
release_date = factory.Faker('date')
|
||||||
cover = factory.django.ImageField()
|
cover = factory.django.ImageField()
|
||||||
artist = factory.SubFactory(ArtistFactory)
|
artist = factory.SubFactory(ArtistFactory)
|
||||||
|
release_group_id = factory.Faker('uuid4')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = 'music.Album'
|
model = 'music.Album'
|
||||||
|
@ -41,3 +44,18 @@ class TrackFileFactory(factory.django.DjangoModelFactory):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = 'music.TrackFile'
|
model = 'music.TrackFile'
|
||||||
|
|
||||||
|
|
||||||
|
class ImportBatchFactory(factory.django.DjangoModelFactory):
|
||||||
|
submitted_by = factory.SubFactory(UserFactory)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = 'music.ImportBatch'
|
||||||
|
|
||||||
|
|
||||||
|
class ImportJobFactory(factory.django.DjangoModelFactory):
|
||||||
|
batch = factory.SubFactory(ImportBatchFactory)
|
||||||
|
source = factory.Faker('url')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = 'music.ImportJob'
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from funkwhale_api.music import models
|
||||||
|
from funkwhale_api.music import importers
|
||||||
|
from . import factories
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_store_release_group_id_on_album(db):
|
||||||
|
album = factories.AlbumFactory()
|
||||||
|
assert album.release_group_id is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_album_stores_release_group(db):
|
||||||
|
|
||||||
|
album_data = {
|
||||||
|
"artist-credit": [
|
||||||
|
{
|
||||||
|
"artist": {
|
||||||
|
"disambiguation": "George Shaw",
|
||||||
|
"id": "62c3befb-6366-4585-b256-809472333801",
|
||||||
|
"name": "Adhesive Wombat",
|
||||||
|
"sort-name": "Wombat, Adhesive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artist-credit-phrase": "Adhesive Wombat",
|
||||||
|
"country": "XW",
|
||||||
|
"date": "2013-06-05",
|
||||||
|
"id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e",
|
||||||
|
"status": "Official",
|
||||||
|
"title": "Marsupial Madness",
|
||||||
|
'release-group': {'id': '447b4979-2178-405c-bfe6-46bf0b09e6c7'}
|
||||||
|
}
|
||||||
|
artist = factories.ArtistFactory(
|
||||||
|
mbid=album_data['artist-credit'][0]['artist']['id']
|
||||||
|
)
|
||||||
|
cleaned_data = models.Album.clean_musicbrainz_data(album_data)
|
||||||
|
album = importers.load(models.Album, cleaned_data, album_data, import_hooks=[])
|
||||||
|
|
||||||
|
assert album.release_group_id == album_data['release-group']['id']
|
||||||
|
assert album.artist == artist
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_job_is_bound_to_track_file(db, mocker):
|
||||||
|
track = factories.TrackFactory()
|
||||||
|
job = factories.ImportJobFactory(mbid=track.mbid)
|
||||||
|
|
||||||
|
mocker.patch('funkwhale_api.music.models.TrackFile.download_file')
|
||||||
|
job.run()
|
||||||
|
job.refresh_from_db()
|
||||||
|
assert job.track_file.track == track
|
|
@ -72,7 +72,10 @@ class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
class ImportBatchViewSet(viewsets.ReadOnlyModelViewSet):
|
class ImportBatchViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = models.ImportBatch.objects.all().order_by('-creation_date')
|
queryset = (
|
||||||
|
models.ImportBatch.objects.all()
|
||||||
|
.prefetch_related('jobs__track_file')
|
||||||
|
.order_by('-creation_date'))
|
||||||
serializer_class = serializers.ImportBatchSerializer
|
serializer_class = serializers.ImportBatchSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import glob
|
import glob
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from funkwhale_api.providers.audiofile import importer
|
from funkwhale_api.providers.audiofile import tasks
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -61,7 +61,7 @@ class Command(BaseCommand):
|
||||||
for path in matching:
|
for path in matching:
|
||||||
self.stdout.write(message.format(path))
|
self.stdout.write(message.format(path))
|
||||||
try:
|
try:
|
||||||
importer.from_path(path)
|
tasks.from_path(path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.stdout.write('Error: {}'.format(e))
|
self.stdout.write('Error: {}'.format(e))
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import datetime
|
||||||
import unittest
|
import unittest
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
|
|
||||||
from funkwhale_api.providers.audiofile import importer
|
from funkwhale_api.providers.audiofile import tasks
|
||||||
|
|
||||||
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class TestAudioFile(TestCase):
|
||||||
return_value='OggVorbis',
|
return_value='OggVorbis',
|
||||||
)
|
)
|
||||||
with m1, m2:
|
with m1, m2:
|
||||||
track_file = importer.from_path(
|
track_file = tasks.from_path(
|
||||||
os.path.join(DATA_DIR, 'dummy_file.ogg'))
|
os.path.join(DATA_DIR, 'dummy_file.ogg'))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-12-14 22:05
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='user',
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='username',
|
||||||
|
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,8 +2,11 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
|
||||||
|
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
|
|
@ -1,46 +1,9 @@
|
||||||
##basic build dependencies of various Django apps for Ubuntu 14.04
|
|
||||||
#build-essential metapackage install: make, gcc, g++,
|
|
||||||
build-essential
|
build-essential
|
||||||
#required to translate
|
|
||||||
gettext
|
gettext
|
||||||
#python-dev
|
|
||||||
|
|
||||||
##shared dependencies of:
|
|
||||||
##Pillow, pylibmc
|
|
||||||
zlib1g-dev
|
zlib1g-dev
|
||||||
|
libjpeg-dev
|
||||||
##Postgresql and psycopg2 dependencies
|
zlib1g-dev
|
||||||
libpq-dev
|
libpq-dev
|
||||||
postgresql-client
|
postgresql-client
|
||||||
##Pillow dependencies
|
|
||||||
#libtiff4-dev
|
|
||||||
#libjpeg8-dev
|
|
||||||
#libfreetype6-dev
|
|
||||||
#liblcms1-dev
|
|
||||||
#libwebp-dev
|
|
||||||
|
|
||||||
|
|
||||||
##django-extensions
|
|
||||||
#graphviz-dev
|
|
||||||
|
|
||||||
##hitch
|
|
||||||
#python-setuptools
|
|
||||||
#python3-dev
|
|
||||||
#python-virtualenv
|
|
||||||
#python-pip
|
|
||||||
#firefox
|
|
||||||
#automake
|
|
||||||
#libtool
|
|
||||||
#libreadline6
|
|
||||||
#libreadline6-dev
|
|
||||||
#libreadline-dev
|
|
||||||
libsqlite3-dev
|
|
||||||
#libxml2
|
|
||||||
#libxml2-dev
|
|
||||||
#libssl-dev
|
|
||||||
#libbz2-dev
|
|
||||||
#wget
|
|
||||||
#curl
|
|
||||||
#llvm
|
|
||||||
|
|
||||||
libav-tools
|
libav-tools
|
||||||
|
python3-dev
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
flake8==2.5.0
|
flake8==2.5.0
|
||||||
model-mommy==1.3.2
|
model-mommy==1.3.2
|
||||||
tox==2.7.0
|
|
||||||
pytest
|
pytest
|
||||||
pytest-django
|
pytest-django
|
||||||
|
pytest-mock
|
||||||
pytest-sugar
|
pytest-sugar
|
||||||
pytest-xdist
|
pytest-xdist
|
||||||
|
|
|
@ -4,3 +4,5 @@ test:
|
||||||
command: pytest
|
command: pytest
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
|
environment:
|
||||||
|
- "DATABASE_URL=sqlite://"
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
# If you're tweaking this file from the template, ensure you edit at lest the
|
# If you're tweaking this file from the template, ensure you edit at least the
|
||||||
# following variables:
|
# following variables:
|
||||||
# - DJANGO_SECRET_KEY
|
# - DJANGO_SECRET_KEY
|
||||||
# - DJANGO_ALLOWED_HOSTS
|
# - DJANGO_ALLOWED_HOSTS
|
||||||
|
|
||||||
|
# Additionaly, on non-docker setup, you'll also have to tweak/uncomment those
|
||||||
|
# variables:
|
||||||
|
# - DATABASE_URL
|
||||||
|
# - CACHE_URL
|
||||||
|
# - STATIC_ROOT
|
||||||
|
# - MEDIA_ROOT
|
||||||
|
|
||||||
# Docker only
|
# Docker only
|
||||||
# -----------
|
# -----------
|
||||||
|
|
||||||
# The tag of the image we should use
|
# The tag of the image we should use
|
||||||
# (it will be interpolated in docker-compose file)
|
# (it will be interpolated in docker-compose file)
|
||||||
|
# You can comment or ignore this if you're not using docker
|
||||||
FUNKWHALE_VERSION=latest
|
FUNKWHALE_VERSION=latest
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,11 +25,41 @@ FUNKWHALE_VERSION=latest
|
||||||
# Set this variables to bind the API server to another interface/port
|
# Set this variables to bind the API server to another interface/port
|
||||||
# example: FUNKWHALE_API_IP=0.0.0.0
|
# example: FUNKWHALE_API_IP=0.0.0.0
|
||||||
# example: FUNKWHALE_API_PORT=5678
|
# example: FUNKWHALE_API_PORT=5678
|
||||||
FUNKWHALE_API_IP=
|
FUNKWHALE_API_IP=127.0.0.1
|
||||||
FUNKWHALE_API_PORT=
|
FUNKWHALE_API_PORT=5000
|
||||||
|
|
||||||
|
|
||||||
# API/Django configuration
|
# API/Django configuration
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
# Examples:
|
||||||
|
# DATABASE_URL=postgresql://<user>:<password>@<host>:<port>/<database>
|
||||||
|
# DATABASE_URL=postgresql://funkwhale:passw0rd@localhost:5432/funkwhale_database
|
||||||
|
# Use the next one if you followed Debian installation guide
|
||||||
|
# DATABASE_URL=postgresql://funkwhale@:5432/funkwhale
|
||||||
|
|
||||||
|
# Cache configuration
|
||||||
|
# Examples:
|
||||||
|
# CACHE_URL=redis://<host>:<port>/<database>
|
||||||
|
# CACHE_URL=redis://localhost:6379/0
|
||||||
|
# Use the next one if you followed Debian installation guide
|
||||||
|
# CACHE_URL=redis://127.0.0.1:6379/0
|
||||||
|
|
||||||
|
# Where media files (such as album covers or audio tracks) should be stored
|
||||||
|
# on your system?
|
||||||
|
# (Ensure this directory actually exists)
|
||||||
|
# MEDIA_ROOT=/srv/funkwhale/data/media
|
||||||
|
|
||||||
|
# Where static files (such as API css or icons) should be compiled
|
||||||
|
# on your system?
|
||||||
|
# (Ensure this directory actually exists)
|
||||||
|
# STATIC_ROOT=/srv/funkwhale/data/static
|
||||||
|
|
||||||
|
# Update it to match the domain that will be used to reach your funkwhale
|
||||||
|
# instance
|
||||||
|
# Example: DJANGO_ALLOWED_HOSTS=funkwhale.yourdomain.com
|
||||||
|
DJANGO_ALLOWED_HOSTS=yourdomain
|
||||||
|
|
||||||
# which settings module should django use?
|
# which settings module should django use?
|
||||||
# You don't have to touch this unless you really know what you're doing
|
# You don't have to touch this unless you really know what you're doing
|
||||||
DJANGO_SETTINGS_MODULE=config.settings.production
|
DJANGO_SETTINGS_MODULE=config.settings.production
|
||||||
|
@ -29,13 +67,9 @@ DJANGO_SETTINGS_MODULE=config.settings.production
|
||||||
# Generate one using `openssl rand -base64 45`, for example
|
# Generate one using `openssl rand -base64 45`, for example
|
||||||
DJANGO_SECRET_KEY=
|
DJANGO_SECRET_KEY=
|
||||||
|
|
||||||
# You don't have to edit this
|
# You don't have to edit this, but you can put the admin on another URL if you
|
||||||
DJANGO_ADMIN_URL=^api/admin/
|
# want to
|
||||||
|
# DJANGO_ADMIN_URL=^api/admin/
|
||||||
# Update it to match the domain that will be used to reach your funkwhale
|
|
||||||
# instance
|
|
||||||
# Example: DJANGO_ALLOWED_HOSTS=funkwhale.yourdomain.com
|
|
||||||
DJANGO_ALLOWED_HOSTS=yourdomain
|
|
||||||
|
|
||||||
# If True, unauthenticated users won't be able to query the API
|
# If True, unauthenticated users won't be able to query the API
|
||||||
API_AUTHENTICATION_REQUIRED=True
|
API_AUTHENTICATION_REQUIRED=True
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Funkwhale application server
|
||||||
|
After=redis.service postgresql.service
|
||||||
|
PartOf=funkwhale.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
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/gunicorn config.wsgi:application -b ${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Funkwhale celery worker
|
||||||
|
After=redis.service postgresql.service
|
||||||
|
PartOf=funkwhale.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
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
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,3 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Funkwhale
|
||||||
|
Wants=funkwhale-server.service funkwhale-worker.service
|
|
@ -1,29 +1,39 @@
|
||||||
|
# Ensure you update at least the server_name variables to match your own
|
||||||
|
# domain
|
||||||
|
|
||||||
upstream funkwhale-api {
|
upstream funkwhale-api {
|
||||||
# depending on your setup, you may want to udpate this
|
# depending on your setup, you may want to udpate this
|
||||||
server localhost:5000;
|
server localhost:5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
server_name demo.funkwhale.audio;
|
# update this to match your instance name
|
||||||
# useful for Let's Encrypt
|
server_name demo.funkwhale.audio;
|
||||||
location /.well-known/acme-challenge/ { allow all; }
|
# useful for Let's Encrypt
|
||||||
location / { return 301 https://$host$request_uri; }
|
location /.well-known/acme-challenge/ { allow all; }
|
||||||
|
location / { return 301 https://$host$request_uri; }
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2;
|
||||||
listen [::]:443 ssl http2;
|
listen [::]:443 ssl http2;
|
||||||
|
# update this to match your instance name
|
||||||
server_name demo.funkwhale.audio;
|
server_name demo.funkwhale.audio;
|
||||||
|
|
||||||
# TLS
|
# TLS
|
||||||
|
# Feel free to use your own configuration for SSL here or simply remove the
|
||||||
|
# lines and move the configuration to the previous server block if you
|
||||||
|
# don't want to run funkwhale behind https (this is not recommanded)
|
||||||
|
# have a look here for let's encrypt configuration:
|
||||||
|
# https://certbot.eff.org/all-instructions/#debian-9-stretch-nginx
|
||||||
ssl_protocols TLSv1.2;
|
ssl_protocols TLSv1.2;
|
||||||
ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
|
ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/demo.funkwhale.audio/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/demo.funkwhale.audio/privkey.pem;
|
||||||
# HSTS
|
# HSTS
|
||||||
add_header Strict-Transport-Security "max-age=31536000";
|
add_header Strict-Transport-Security "max-age=31536000";
|
||||||
|
|
||||||
|
|
14
dev.yml
14
dev.yml
|
@ -33,7 +33,12 @@ services:
|
||||||
- redis
|
- redis
|
||||||
command: python manage.py celery worker
|
command: python manage.py celery worker
|
||||||
environment:
|
environment:
|
||||||
- C_FORCE_ROOT=true
|
- "DJANGO_ALLOWED_HOSTS=localhost"
|
||||||
|
- "DJANGO_SETTINGS_MODULE=config.settings.local"
|
||||||
|
- "DJANGO_SECRET_KEY=dev"
|
||||||
|
- C_FORCE_ROOT=true
|
||||||
|
- "DATABASE_URL=postgresql://postgres@postgres/postgres"
|
||||||
|
- "CACHE_URL=redis://redis:6379/0"
|
||||||
volumes:
|
volumes:
|
||||||
- ./api:/app
|
- ./api:/app
|
||||||
- ./data/music:/music
|
- ./data/music:/music
|
||||||
|
@ -46,12 +51,17 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./api:/app
|
- ./api:/app
|
||||||
- ./data/music:/music
|
- ./data/music:/music
|
||||||
|
environment:
|
||||||
|
- "DJANGO_ALLOWED_HOSTS=localhost"
|
||||||
|
- "DJANGO_SETTINGS_MODULE=config.settings.local"
|
||||||
|
- "DJANGO_SECRET_KEY=dev"
|
||||||
|
- "DATABASE_URL=postgresql://postgres@postgres/postgres"
|
||||||
|
- "CACHE_URL=redis://redis:6379/0"
|
||||||
ports:
|
ports:
|
||||||
- "12081:12081"
|
- "12081:12081"
|
||||||
links:
|
links:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
- celeryworker
|
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
env_file: .env.dev
|
env_file: .env.dev
|
||||||
|
|
|
@ -21,9 +21,7 @@ Changelog
|
||||||
* [feature] can now import artist and releases from youtube and musicbrainz.
|
* [feature] can now import artist and releases from youtube and musicbrainz.
|
||||||
This requires a YouTube API key for the search
|
This requires a YouTube API key for the search
|
||||||
* [breaking] we now check for user permission before serving audio files, which requires
|
* [breaking] we now check for user permission before serving audio files, which requires
|
||||||
a specific configuration block in your reverse proxy configuration:
|
a specific configuration block in your reverse proxy configuration::
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
location /_protected/media {
|
location /_protected/media {
|
||||||
internal;
|
internal;
|
||||||
|
|
17
docs/conf.py
17
docs/conf.py
|
@ -17,10 +17,12 @@
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
#
|
#
|
||||||
# import os
|
import os
|
||||||
# import sys
|
import sys
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath('../api'))
|
||||||
|
|
||||||
|
import funkwhale_api # NOQA
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
@ -55,9 +57,11 @@ author = 'Eliot Berriot'
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.1'
|
# version = funkwhale_api.__version__
|
||||||
|
# @TODO use real version here
|
||||||
|
version = 'feature/22-debian-installation'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.1'
|
release = version
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
@ -152,6 +156,3 @@ texinfo_documents = [
|
||||||
author, 'funkwhale', 'One line description of project.',
|
author, 'funkwhale', 'One line description of project.',
|
||||||
'Miscellaneous'),
|
'Miscellaneous'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,18 @@ From music directory on the server
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
You can import music files in funkwhale assuming they are located on the server
|
You can import music files in funkwhale assuming they are located on the server
|
||||||
and readable by the funkwhale application.
|
and readable by the funkwhale application. Your music files should contain at
|
||||||
|
least an ``artist``, ``album`` and ``title`` tags.
|
||||||
|
|
||||||
Assuming your music is located at ``/music`` and your music files contains at
|
You can import those tracks as follows, assuming they are located in
|
||||||
least an ``artist``, ``album`` and ``title`` tag, you can import those tracks as follows:
|
``/srv/funkwhale/data/music``:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python api/manage.py import_files "/srv/funkwhale/data/music/**/*.ogg" --recursive --noinput
|
||||||
|
|
||||||
|
When you use docker, the ``/srv/funkwhale/data/music`` is mounted from the host
|
||||||
|
to the ``/music`` directory on the container:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
@ -17,6 +25,7 @@ least an ``artist``, ``album`` and ``title`` tag, you can import those tracks as
|
||||||
For the best results, we recommand tagging your music collection through
|
For the best results, we recommand tagging your music collection through
|
||||||
`Picard <http://picard.musicbrainz.org/>`_ in order to have the best quality metadata.
|
`Picard <http://picard.musicbrainz.org/>`_ in order to have the best quality metadata.
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
This command is idempotent, meaning you can run it multiple times on the same
|
This command is idempotent, meaning you can run it multiple times on the same
|
||||||
|
@ -26,6 +35,18 @@ For the best results, we recommand tagging your music collection through
|
||||||
|
|
||||||
At the moment, only OGG/Vorbis and MP3 files with ID3 tags are supported
|
At the moment, only OGG/Vorbis and MP3 files with ID3 tags are supported
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The --recursive flag will work only on Python 3.5+, which is the default
|
||||||
|
version When using Docker or Debian 9. If you use an older version of Python,
|
||||||
|
remove the --recursive flag and use more explicit import patterns instead::
|
||||||
|
|
||||||
|
# this will only import ogg files at the second level
|
||||||
|
"/srv/funkwhale/data/music/*/*.ogg"
|
||||||
|
# this will only import ogg files in the fiven directory
|
||||||
|
"/srv/funkwhale/data/music/System-of-a-down/*.ogg"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Getting demo tracks
|
Getting demo tracks
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -34,10 +55,10 @@ If you do not have any music on your server but still want to test the import
|
||||||
process, you can call the following methods do download a few albums licenced
|
process, you can call the following methods do download a few albums licenced
|
||||||
under creative commons (courtesy of Jamendo):
|
under creative commons (courtesy of Jamendo):
|
||||||
|
|
||||||
.. code-block:: bash
|
.. parsed-literal::
|
||||||
|
|
||||||
curl -L -o download-tracks.sh "https://code.eliotberriot.com/funkwhale/funkwhale/raw/master/demo/download-tracks.sh"
|
curl -L -o download-tracks.sh "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/demo/download-tracks.sh"
|
||||||
curl -L -o music.txt "https://code.eliotberriot.com/funkwhale/funkwhale/raw/master/demo/music.txt"
|
curl -L -o music.txt "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/demo/music.txt"
|
||||||
chmod +x download-tracks.sh
|
chmod +x download-tracks.sh
|
||||||
./download-tracks.sh music.txt
|
./download-tracks.sh music.txt
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,280 @@
|
||||||
|
Debian installation
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
this guide targets Debian 9, which is the latest debian, but should work
|
||||||
|
similarly on Debian 8.
|
||||||
|
|
||||||
|
External dependencies
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The guides will focus on installing funkwhale-specific components and
|
||||||
|
dependencies. However, funkwhale requires a
|
||||||
|
:doc:`few external dependencies <./external_dependencies>` for which
|
||||||
|
documentation is outside of this document scope.
|
||||||
|
|
||||||
|
Install utilities
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
You'll need a few utilities during this guide that are not always present by
|
||||||
|
default on system. You can install them using:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install curl python3-venv git unzip
|
||||||
|
|
||||||
|
|
||||||
|
Layout
|
||||||
|
-------
|
||||||
|
|
||||||
|
All funkwhale-related files will be located under ``/srv/funkwhale`` apart
|
||||||
|
from database files and a few configuration files. We will also have a
|
||||||
|
dedicated ``funwhale`` user to launch the processes we need and own those files.
|
||||||
|
|
||||||
|
You are free to use different values here, just remember to adapt those in the
|
||||||
|
next steps.
|
||||||
|
|
||||||
|
.. _create-funkwhale-user:
|
||||||
|
|
||||||
|
Create the user and the directory:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo adduser --system --home /srv/funkwhale funkwhale
|
||||||
|
cd /srv/funkwhale
|
||||||
|
|
||||||
|
Log in as the newly created user from now on:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo -u funkwhale -H bash
|
||||||
|
|
||||||
|
Now let's setup our directory layout. Here is how it will look like::
|
||||||
|
|
||||||
|
.
|
||||||
|
├── config # config / environment files
|
||||||
|
├── api # api code of your instance
|
||||||
|
├── data # persistent data, such as music files
|
||||||
|
├── front # frontend files for the web user interface
|
||||||
|
└── virtualenv # python dependencies for funkwhale
|
||||||
|
|
||||||
|
Create the aforementionned directories:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
mkdir -p config api data/static data/media data/music front
|
||||||
|
|
||||||
|
The ``virtualenv`` directory is a bit special and will be created separately.
|
||||||
|
|
||||||
|
Download latest funkwhale release
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Funkwhale is splitted in two components:
|
||||||
|
|
||||||
|
1. The API, which will handle music storage and user accounts
|
||||||
|
2. The frontend, that will simply connect to the API to interact with its data
|
||||||
|
|
||||||
|
Those components are packaged in subsequent releases, such as 0.1, 0.2, etc.
|
||||||
|
You can browse the :doc:`changelog </changelog>` for a list of available releases
|
||||||
|
and pick the one you want to install, usually the latest one should be okay.
|
||||||
|
|
||||||
|
In this guide, we'll assume you want to install the latest version of funkwhale,
|
||||||
|
which is |version|:
|
||||||
|
|
||||||
|
First, we'll download the latest api release.
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
curl -L -o "api-|version|.zip" "https://code.eliotberriot.com/funkwhale/funkwhale/-/jobs/artifacts/|version|/download?job=build_api"
|
||||||
|
unzip "api-|version|.zip" -d extracted
|
||||||
|
mv extracted/api api
|
||||||
|
rmdir extracted
|
||||||
|
|
||||||
|
|
||||||
|
Then we'll download the frontend files:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
curl -L -o "front-|version|.zip" "https://code.eliotberriot.com/funkwhale/funkwhale/-/jobs/artifacts/|version|/download?job=build_front"
|
||||||
|
unzip "front-|version|.zip" -d extracted
|
||||||
|
mv extracted/front .
|
||||||
|
rmdir extracted
|
||||||
|
|
||||||
|
You can leave the ZIP archives in the directory, this will help you know
|
||||||
|
which version you've installed next time you want to upgrade your installation.
|
||||||
|
|
||||||
|
System dependencies
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
First, switch to the api directory:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
cd api
|
||||||
|
|
||||||
|
A few OS packages are required in order to run funkwhale. The list is available
|
||||||
|
in ``api/requirements.apt`` or by running
|
||||||
|
``./install_os_dependencies.sh list``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Ensure you are running the next commands as root or using sudo
|
||||||
|
(and not as the funkwhale) user.
|
||||||
|
|
||||||
|
You can install those packages all at once:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
./install_os_dependencies.sh install
|
||||||
|
|
||||||
|
From now on you can switch back to the funkwhale user.
|
||||||
|
|
||||||
|
Python dependencies
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Go back to the base directory:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
cd /srv/funkwhale
|
||||||
|
|
||||||
|
To avoid collisions with other software on your system, Python dependencies
|
||||||
|
will be installed in a dedicated
|
||||||
|
`virtualenv <https://docs.python.org/3/library/venv.html>`_.
|
||||||
|
|
||||||
|
First, create the virtualenv:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
python3 -m venv /srv/funkwhale/virtualenv
|
||||||
|
|
||||||
|
This will result in a ``virtualenv`` directory being created in
|
||||||
|
``/srv/funkwhale/virtualenv``.
|
||||||
|
|
||||||
|
In the rest of this guide, we'll need to activate this environment to ensure
|
||||||
|
dependencies are installed within it, and not directly on your host system.
|
||||||
|
|
||||||
|
This is done with the following command:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
source /srv/funkwhale/virtualenv/bin/activate
|
||||||
|
|
||||||
|
Finally, install the python dependencies:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
pip install wheel
|
||||||
|
pip install -r api/requirements.txt
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
further commands involving python should always be run after you activated
|
||||||
|
the virtualenv, as described earlier, otherwise those commands will raise
|
||||||
|
errors
|
||||||
|
|
||||||
|
|
||||||
|
Environment file
|
||||||
|
----------------
|
||||||
|
|
||||||
|
You can now start to configure funkwhale. The main way to achieve that is by
|
||||||
|
adding an environment file that will host settings that are relevant to your
|
||||||
|
installation.
|
||||||
|
|
||||||
|
Download the sample environment file:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
curl -L -o config/.env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/env.prod.sample"
|
||||||
|
|
||||||
|
You can then edit it: the file is heavily commented, and the most relevant
|
||||||
|
configuration options are mentionned at the top of the file.
|
||||||
|
|
||||||
|
Especially, populate the ``DATABASE_URL`` and ``CACHE_URL`` values based on
|
||||||
|
how you configured your PostgreSQL and Redis servers in
|
||||||
|
:doc:`external dependencies <./external_dependencies>`.
|
||||||
|
|
||||||
|
|
||||||
|
When you want to run command on the API server, such as to create the
|
||||||
|
database or compile static files, you have to ensure you source
|
||||||
|
the environment variables.
|
||||||
|
|
||||||
|
This can be done like this::
|
||||||
|
|
||||||
|
export $(cat config/.env | grep -v ^# | xargs)
|
||||||
|
|
||||||
|
The easier thing to do is to store this in a script::
|
||||||
|
|
||||||
|
cat > /srv/funkwhale/load_env <<'EOL'
|
||||||
|
#!/bin/bash
|
||||||
|
export $(cat /srv/funkwhale/config/.env | grep -v ^# | xargs)
|
||||||
|
EOL
|
||||||
|
chmod +x /srv/funkwhale/load_env
|
||||||
|
|
||||||
|
You should now be able to run the following to populate your environment
|
||||||
|
variables easily:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
source /srv/funkwhale/load_env
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Remember to source ``load_env`` whenever you edit your .env file.
|
||||||
|
|
||||||
|
Database setup
|
||||||
|
---------------
|
||||||
|
|
||||||
|
You should now be able to import the initial database structure:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
python api/manage.py migrate
|
||||||
|
|
||||||
|
This will create the required tables and rows.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You can safely execute this command any time you want, this will only
|
||||||
|
run unapplied migrations.
|
||||||
|
|
||||||
|
|
||||||
|
Create an admin account
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
You can then create your first user account:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
python api/manage.py createsuperuser
|
||||||
|
|
||||||
|
If you ever want to change a user's password from the command line, just run:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
python api/manage.py changepassword <user>
|
||||||
|
|
||||||
|
Collect static files
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Static files are the static assets used by the API server (icon PNGs, CSS, etc.).
|
||||||
|
We need to collect them explicitely, so they can be served by the webserver:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
python api/manage.py collectstatic
|
||||||
|
|
||||||
|
This should populate the directory you choose for the ``STATIC_ROOT`` variable
|
||||||
|
in your ``.env`` file.
|
||||||
|
|
||||||
|
Systemd unit file
|
||||||
|
------------------
|
||||||
|
|
||||||
|
See :doc:`./systemd`.
|
||||||
|
|
||||||
|
Reverse proxy setup
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
See :ref:`reverse-proxy <reverse-proxy-setup>`.
|
|
@ -7,17 +7,17 @@ First, ensure you have `Docker <https://docs.docker.com/engine/installation/>`_
|
||||||
|
|
||||||
Download the sample docker-compose file:
|
Download the sample docker-compose file:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. parsed-literal::
|
||||||
|
|
||||||
mkdir -p /srv/funkwhale
|
mkdir -p /srv/funkwhale
|
||||||
cd /srv/funkwhale
|
cd /srv/funkwhale
|
||||||
curl -L -o docker-compose.yml "https://code.eliotberriot.com/funkwhale/funkwhale/raw/master/deploy/docker-compose.yml"
|
curl -L -o docker-compose.yml "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/docker-compose.yml"
|
||||||
|
|
||||||
Create your env file:
|
Create your env file:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. parsed-literal::
|
||||||
|
|
||||||
curl -L -o .env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/master/deploy/env.prod.sample"
|
curl -L -o .env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/env.prod.sample"
|
||||||
|
|
||||||
Ensure to edit it to match your needs (this file is heavily commented)
|
Ensure to edit it to match your needs (this file is heavily commented)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
External dependencies
|
||||||
|
=====================
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Those dependencies are handled automatically if you are
|
||||||
|
:doc:`deploying using docker <./docker>`
|
||||||
|
|
||||||
|
Database setup (PostgreSQL)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Funkwhale requires a PostgreSQL database to work properly. Please refer
|
||||||
|
to the `PostgreSQL documentation <https://www.postgresql.org/download/>`_
|
||||||
|
for installation instructions specific to your os.
|
||||||
|
|
||||||
|
On debian-like systems, you would install the database server like this:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo apt-get install postgresql
|
||||||
|
|
||||||
|
The remaining steps are heavily inspired from `this Digital Ocean guide <https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04>`_.
|
||||||
|
|
||||||
|
Open a database shell:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo -u postgres psql
|
||||||
|
|
||||||
|
Create the project database and user:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
CREATE DATABASE funkwhale;
|
||||||
|
CREATE USER funkwhale;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE funkwhale TO funkwhale;
|
||||||
|
|
||||||
|
Assuming you already have :ref:`created your funkwhale user <create-funkwhale-user>`,
|
||||||
|
you should now be able to open a postgresql shell:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo -u funkwhale -H psql
|
||||||
|
|
||||||
|
Cache setup (Redis)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Funkwhale also requires a cache server:
|
||||||
|
|
||||||
|
- To make the whole system faster, by caching network requests or database
|
||||||
|
queries
|
||||||
|
- To handle asynchronous tasks such as music import
|
||||||
|
|
||||||
|
On debian-like distributions, a redis package is available, and you can
|
||||||
|
install it:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo apt-get install redis-server
|
||||||
|
|
||||||
|
This should be enough to have your redis server set up.
|
|
@ -18,7 +18,10 @@ Available installation methods
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
|
external_dependencies
|
||||||
|
debian
|
||||||
docker
|
docker
|
||||||
|
systemd
|
||||||
|
|
||||||
|
|
||||||
.. _frontend-setup:
|
.. _frontend-setup:
|
||||||
|
@ -33,10 +36,10 @@ Frontend setup
|
||||||
|
|
||||||
Files for the web frontend are purely static and can simply be downloaded, unzipped and served from any webserver:
|
Files for the web frontend are purely static and can simply be downloaded, unzipped and served from any webserver:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. parsed-literal::
|
||||||
|
|
||||||
cd /srv/funkwhale
|
cd /srv/funkwhale
|
||||||
curl -L -o front.zip "https://code.eliotberriot.com/funkwhale/funkwhale/builds/artifacts/master/download?job=build_front"
|
curl -L -o front.zip "https://code.eliotberriot.com/funkwhale/funkwhale/builds/artifacts/|version|/download?job=build_front"
|
||||||
unzip front.zip
|
unzip front.zip
|
||||||
|
|
||||||
.. _reverse-proxy-setup:
|
.. _reverse-proxy-setup:
|
||||||
|
@ -58,8 +61,8 @@ Ensure you have a recent version of nginx on your server. On debian-like system,
|
||||||
|
|
||||||
Then, download our sample virtualhost file:
|
Then, download our sample virtualhost file:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. parsed-literal::
|
||||||
|
|
||||||
curl -L -o /etc/nginx/sites-enabled/funkwhale.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/master/deploy/nginx.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``.
|
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``.
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
Systemd configuration
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Systemd offers a convenient way to manage your funkwhale instance if you're
|
||||||
|
not using docker.
|
||||||
|
|
||||||
|
We'll see how to setup systemd to proprely start a funkwhale instance.
|
||||||
|
|
||||||
|
First, download the sample unitfiles:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
curl -L -o "/etc/systemd/system/funkwhale.target" "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale.target"
|
||||||
|
curl -L -o "/etc/systemd/system/funkwhale-server.service" "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale-server.service"
|
||||||
|
curl -L -o "/etc/systemd/system/funkwhale-worker.service" "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale-worker.service"
|
||||||
|
|
||||||
|
This will download three unitfiles:
|
||||||
|
|
||||||
|
- ``funkwhale-server.service`` to launch the funkwhale web server
|
||||||
|
- ``funkwhale-worker.service`` to launch the funkwhale task worker
|
||||||
|
- ``funkwhale.target`` to easily stop and start all of the services at once
|
||||||
|
|
||||||
|
You can of course review and edit them to suit your deployment scenario
|
||||||
|
if needed, but the defaults should be fine.
|
||||||
|
|
||||||
|
Once the files are downloaded, reload systemd:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
And start the services:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
systemctl start funkwhale.target
|
||||||
|
|
||||||
|
You can check the statuses of all processes like this:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
systemctl status funkwhale-\*
|
|
@ -116,7 +116,7 @@ class Queue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
append (track, index) {
|
append (track, index, skipPlay) {
|
||||||
this.previousQueue = null
|
this.previousQueue = null
|
||||||
index = index || this.tracks.length
|
index = index || this.tracks.length
|
||||||
if (index > this.tracks.length - 1) {
|
if (index > this.tracks.length - 1) {
|
||||||
|
@ -126,20 +126,32 @@ class Queue {
|
||||||
// we insert the track at given position
|
// we insert the track at given position
|
||||||
this.tracks.splice(index, 0, track)
|
this.tracks.splice(index, 0, track)
|
||||||
}
|
}
|
||||||
if (this.ended) {
|
if (!skipPlay) {
|
||||||
logger.default.debug('Playing appended track')
|
this.resumeQueue()
|
||||||
this.play(this.currentIndex + 1)
|
|
||||||
}
|
}
|
||||||
this.cache()
|
this.cache()
|
||||||
}
|
}
|
||||||
|
|
||||||
appendMany (tracks, index) {
|
appendMany (tracks, index) {
|
||||||
|
logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
|
||||||
let self = this
|
let self = this
|
||||||
index = index || this.tracks.length - 1
|
if (this.tracks.length === 0) {
|
||||||
|
index = 0
|
||||||
|
} else {
|
||||||
|
index = index || this.tracks.length
|
||||||
|
}
|
||||||
|
console.log('INDEEEEEX', index)
|
||||||
tracks.forEach((t) => {
|
tracks.forEach((t) => {
|
||||||
self.append(t, index)
|
self.append(t, index, true)
|
||||||
index += 1
|
index += 1
|
||||||
})
|
})
|
||||||
|
this.resumeQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeQueue () {
|
||||||
|
if (this.ended | this.errored) {
|
||||||
|
this.next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
populateFromRadio () {
|
populateFromRadio () {
|
||||||
|
@ -185,15 +197,24 @@ class Queue {
|
||||||
}
|
}
|
||||||
|
|
||||||
stop () {
|
stop () {
|
||||||
this.audio.pause()
|
if (this.audio.pause) {
|
||||||
this.audio.destroyed()
|
this.audio.pause()
|
||||||
|
}
|
||||||
|
if (this.audio.destroyed) {
|
||||||
|
this.audio.destroyed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
play (index) {
|
play (index) {
|
||||||
let self = this
|
let self = this
|
||||||
let currentIndex = index
|
let currentIndex = index
|
||||||
let currentTrack = this.tracks[index]
|
let currentTrack = this.tracks[index]
|
||||||
|
|
||||||
|
if (this.audio.destroyed) {
|
||||||
|
logger.default.debug('Destroying previous audio...', index - 1)
|
||||||
|
this.audio.destroyed()
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentTrack) {
|
if (!currentTrack) {
|
||||||
logger.default.debug('No track at index', index)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,12 +222,13 @@ class Queue {
|
||||||
this.currentTrack = currentTrack
|
this.currentTrack = currentTrack
|
||||||
|
|
||||||
this.ended = false
|
this.ended = false
|
||||||
|
this.errored = false
|
||||||
let file = this.currentTrack.files[0]
|
let file = this.currentTrack.files[0]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
|
this.errored = true
|
||||||
return this.next()
|
return this.next()
|
||||||
}
|
}
|
||||||
let path = backend.absoluteUrl(file.path)
|
let path = backend.absoluteUrl(file.path)
|
||||||
|
|
||||||
if (auth.user.authenticated) {
|
if (auth.user.authenticated) {
|
||||||
// we need to send the token directly in url
|
// we need to send the token directly in url
|
||||||
// so authentication can be checked by the backend
|
// so authentication can be checked by the backend
|
||||||
|
@ -215,10 +237,6 @@ class Queue {
|
||||||
path = url.updateQueryString(path, 'jwt', auth.getAuthToken())
|
path = url.updateQueryString(path, 'jwt', auth.getAuthToken())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.audio.destroyed) {
|
|
||||||
logger.default.debug('Destroying previous audio...', index - 1)
|
|
||||||
this.audio.destroyed()
|
|
||||||
}
|
|
||||||
let audio = new Audio(path, {
|
let audio = new Audio(path, {
|
||||||
preload: true,
|
preload: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
|
@ -271,6 +289,7 @@ class Queue {
|
||||||
|
|
||||||
next () {
|
next () {
|
||||||
if (this.currentIndex < this.tracks.length - 1) {
|
if (this.currentIndex < this.tracks.length - 1) {
|
||||||
|
logger.default.debug('Playing next track')
|
||||||
this.play(this.currentIndex + 1)
|
this.play(this.currentIndex + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,8 +59,8 @@
|
||||||
</div>
|
</div>
|
||||||
<pre>
|
<pre>
|
||||||
export PRIVATE_TOKEN="{{ auth.getAuthToken ()}}"
|
export PRIVATE_TOKEN="{{ auth.getAuthToken ()}}"
|
||||||
<template v-for="track in tracks">
|
<template v-for="track in tracks"><template v-if="track.files.length > 0">
|
||||||
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>
|
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></template>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
|
import backend from '@/audio/backend'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import ArtistCard from '@/components/audio/artist/Card'
|
import ArtistCard from '@/components/audio/artist/Card'
|
||||||
import Pagination from '@/components/Pagination'
|
import Pagination from '@/components/Pagination'
|
||||||
|
@ -66,6 +67,13 @@ export default {
|
||||||
logger.default.debug('Fetching artists')
|
logger.default.debug('Fetching artists')
|
||||||
this.$http.get(url, {params: params}).then((response) => {
|
this.$http.get(url, {params: params}).then((response) => {
|
||||||
self.result = response.data
|
self.result = response.data
|
||||||
|
self.result.results.map((artist) => {
|
||||||
|
var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
|
||||||
|
return backend.Album.clean(album)
|
||||||
|
})
|
||||||
|
artist.albums = albums
|
||||||
|
return artist
|
||||||
|
})
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
<th>Recording MusicBrainz ID</th>
|
<th>Recording MusicBrainz ID</th>
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Track</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -38,6 +39,9 @@
|
||||||
<span
|
<span
|
||||||
:class="['ui', {'yellow': job.status === 'pending'}, {'green': job.status === 'finished'}, 'label']">{{ job.status }}</span>
|
:class="['ui', {'yellow': job.status === 'pending'}, {'green': job.status === 'finished'}, 'label']">{{ job.status }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<router-link v-if="job.track_file" :to="{name: 'library.tracks.detail', params: {id: job.track_file.track }}">{{ job.track_file.track }}</router-link>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
Loading…
Reference in New Issue