Verified Commit 142a8050 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'release/0.2.4'

parents e9a3c37a 9d81ece0
...@@ -18,6 +18,8 @@ test_api: ...@@ -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
......
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 ...@@ -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 ...@@ -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 ...@@ -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' ...@@ -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 = ( ...@@ -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' ...@@ -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( ...@@ -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') ...@@ -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", ) ...@@ -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'] = [ ...@@ -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 = { ...@@ -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 = { ...@@ -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'
......
# -*- 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('.')])
# -*- 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 ...@@ -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']
......
# -*- 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),
),
]
# -*- 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'),
),
]
# -*- 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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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')