Merge branch 'transcoding' into 'develop'
Transcoding Closes #60 See merge request funkwhale/funkwhale!47
This commit is contained in:
commit
953d0ddc91
|
@ -72,7 +72,7 @@ api/music
|
||||||
api/media
|
api/media
|
||||||
api/staticfiles
|
api/staticfiles
|
||||||
api/static
|
api/static
|
||||||
|
api/.pytest_cache
|
||||||
|
|
||||||
# Front
|
# Front
|
||||||
front/node_modules/
|
front/node_modules/
|
||||||
|
|
14
CHANGELOG
14
CHANGELOG
|
@ -6,6 +6,20 @@ Changelog
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
- Front: Now reset player colors when track has no cover (#46)
|
- Front: Now reset player colors when track has no cover (#46)
|
||||||
|
- Front: play button now disabled for unplayable tracks
|
||||||
|
|
||||||
|
Transcoding:
|
||||||
|
|
||||||
|
Basic transcoding is now available to/from the following formats : ogg and mp3.
|
||||||
|
|
||||||
|
*This is still an alpha feature at the moment, please report any bug.*
|
||||||
|
|
||||||
|
This relies internally on FFMPEG and can put some load on your server.
|
||||||
|
It's definitely recommended you setup some caching for the transcoded files
|
||||||
|
at your webserver level. Check the the exemple nginx file at deploy/nginx.conf
|
||||||
|
for an implementation.
|
||||||
|
|
||||||
|
On the frontend, usage of transcoding should be transparent in the player.
|
||||||
|
|
||||||
0.4 (2018-02-18)
|
0.4 (2018-02-18)
|
||||||
----------------
|
----------------
|
||||||
|
|
|
@ -3,7 +3,7 @@ FROM python:3.5
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
# Requirements have to be pulled and installed here, otherwise caching won't work
|
# Requirements have to be pulled and installed here, otherwise caching won't work
|
||||||
|
RUN echo 'deb http://httpredir.debian.org/debian/ jessie-backports main' > /etc/apt/sources.list.d/ffmpeg.list
|
||||||
COPY ./requirements.apt /requirements.apt
|
COPY ./requirements.apt /requirements.apt
|
||||||
RUN apt-get update -qq && grep "^[^#;]" requirements.apt | xargs apt-get install -y
|
RUN apt-get update -qq && grep "^[^#;]" requirements.apt | xargs apt-get install -y
|
||||||
RUN curl -L https://github.com/acoustid/chromaprint/releases/download/v1.4.2/chromaprint-fpcalc-1.4.2-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin --strip 1
|
RUN curl -L https://github.com/acoustid/chromaprint/releases/download/v1.4.2/chromaprint-fpcalc-1.4.2-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin --strip 1
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
FROM python:3.5
|
FROM python:3.5
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
|
||||||
# Requirements have to be pulled and installed here, otherwise caching won't work
|
# Requirements have to be pulled and installed here, otherwise caching won't work
|
||||||
|
RUN echo 'deb http://httpredir.debian.org/debian/ jessie-backports main' > /etc/apt/sources.list.d/ffmpeg.list
|
||||||
COPY ./requirements.apt /requirements.apt
|
COPY ./requirements.apt /requirements.apt
|
||||||
COPY ./install_os_dependencies.sh /install_os_dependencies.sh
|
COPY ./install_os_dependencies.sh /install_os_dependencies.sh
|
||||||
RUN bash install_os_dependencies.sh install
|
RUN bash install_os_dependencies.sh install
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class TranscodeForm(forms.Form):
|
||||||
|
FORMAT_CHOICES = [
|
||||||
|
('ogg', 'ogg'),
|
||||||
|
('mp3', 'mp3'),
|
||||||
|
]
|
||||||
|
|
||||||
|
to = forms.ChoiceField(choices=FORMAT_CHOICES)
|
||||||
|
BITRATE_CHOICES = [
|
||||||
|
(64, '64'),
|
||||||
|
(128, '128'),
|
||||||
|
(256, '256'),
|
||||||
|
]
|
||||||
|
bitrate = forms.ChoiceField(
|
||||||
|
choices=BITRATE_CHOICES, required=False)
|
||||||
|
|
||||||
|
track_file = forms.ModelChoiceField(
|
||||||
|
queryset=models.TrackFile.objects.all()
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 2.0.2 on 2018-02-18 15:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('music', '0017_auto_20171227_1728'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trackfile',
|
||||||
|
name='mimetype',
|
||||||
|
field=models.CharField(blank=True, max_length=200, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importjob',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(max_length=500),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importjob',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('pending', 'Pending'), ('finished', 'Finished'), ('errored', 'Errored'), ('skipped', 'Skipped')], default='pending', max_length=30),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from funkwhale_api.music.utils import guess_mimetype
|
||||||
|
|
||||||
|
|
||||||
|
def populate_mimetype(apps, schema_editor):
|
||||||
|
TrackFile = apps.get_model("music", "TrackFile")
|
||||||
|
|
||||||
|
for tf in TrackFile.objects.filter(audio_file__isnull=False, mimetype__isnull=True).only('audio_file'):
|
||||||
|
try:
|
||||||
|
tf.mimetype = guess_mimetype(tf.audio_file)
|
||||||
|
except Exception as e:
|
||||||
|
print('Error on track file {}: {}'.format(tf.pk, e))
|
||||||
|
continue
|
||||||
|
print('Track file {}: {}'.format(tf.pk, tf.mimetype))
|
||||||
|
tf.save(update_fields=['mimetype'])
|
||||||
|
|
||||||
|
|
||||||
|
def rewind(apps, schema_editor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('music', '0018_auto_20180218_1554'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(populate_mimetype, rewind),
|
||||||
|
]
|
|
@ -18,6 +18,7 @@ from versatileimagefield.fields import VersatileImageField
|
||||||
from funkwhale_api import downloader
|
from funkwhale_api import downloader
|
||||||
from funkwhale_api import musicbrainz
|
from funkwhale_api import musicbrainz
|
||||||
from . import importers
|
from . import importers
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
class APIModelMixin(models.Model):
|
class APIModelMixin(models.Model):
|
||||||
|
@ -364,6 +365,7 @@ class TrackFile(models.Model):
|
||||||
source = models.URLField(null=True, blank=True)
|
source = models.URLField(null=True, blank=True)
|
||||||
duration = models.IntegerField(null=True, blank=True)
|
duration = models.IntegerField(null=True, blank=True)
|
||||||
acoustid_track_id = models.UUIDField(null=True, blank=True)
|
acoustid_track_id = models.UUIDField(null=True, blank=True)
|
||||||
|
mimetype = models.CharField(null=True, blank=True, max_length=200)
|
||||||
|
|
||||||
def download_file(self):
|
def download_file(self):
|
||||||
# import the track file, since there is not any
|
# import the track file, since there is not any
|
||||||
|
@ -393,6 +395,10 @@ class TrackFile(models.Model):
|
||||||
self.track.full_name,
|
self.track.full_name,
|
||||||
os.path.splitext(self.audio_file.name)[-1])
|
os.path.splitext(self.audio_file.name)[-1])
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
if not self.mimetype and self.audio_file:
|
||||||
|
self.mimetype = utils.guess_mimetype(self.audio_file)
|
||||||
|
return super().save(**kwargs)
|
||||||
|
|
||||||
class ImportBatch(models.Model):
|
class ImportBatch(models.Model):
|
||||||
IMPORT_BATCH_SOURCES = [
|
IMPORT_BATCH_SOURCES = [
|
||||||
|
|
|
@ -28,7 +28,14 @@ class TrackFileSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.TrackFile
|
model = models.TrackFile
|
||||||
fields = ('id', 'path', 'duration', 'source', 'filename', 'track')
|
fields = (
|
||||||
|
'id',
|
||||||
|
'path',
|
||||||
|
'duration',
|
||||||
|
'source',
|
||||||
|
'filename',
|
||||||
|
'mimetype',
|
||||||
|
'track')
|
||||||
|
|
||||||
def get_path(self, o):
|
def get_path(self, o):
|
||||||
url = o.path
|
url = o.path
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
import magic
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
def normalize_query(query_string,
|
def normalize_query(query_string,
|
||||||
findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
|
findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
|
||||||
normspace=re.compile(r'\s{2,}').sub):
|
normspace=re.compile(r'\s{2,}').sub):
|
||||||
|
@ -15,6 +17,7 @@ def normalize_query(query_string,
|
||||||
'''
|
'''
|
||||||
return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)]
|
return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)]
|
||||||
|
|
||||||
|
|
||||||
def get_query(query_string, search_fields):
|
def get_query(query_string, search_fields):
|
||||||
''' Returns a query, that is a combination of Q objects. That combination
|
''' Returns a query, that is a combination of Q objects. That combination
|
||||||
aims to search keywords within a model by testing the given search fields.
|
aims to search keywords within a model by testing the given search fields.
|
||||||
|
@ -35,3 +38,8 @@ def get_query(query_string, search_fields):
|
||||||
else:
|
else:
|
||||||
query = query & or_query
|
query = query & or_query
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
def guess_mimetype(f):
|
||||||
|
b = min(100000, f.size)
|
||||||
|
return magic.from_buffer(f.read(b), mime=True)
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
|
import ffmpeg
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import subprocess
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models.functions import Length
|
from django.db.models.functions import Length
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.http import StreamingHttpResponse
|
||||||
|
|
||||||
from rest_framework import viewsets, views, mixins
|
from rest_framework import viewsets, views, mixins
|
||||||
from rest_framework.decorators import detail_route, list_route
|
from rest_framework.decorators import detail_route, list_route
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -19,6 +24,7 @@ from funkwhale_api.common.permissions import (
|
||||||
ConditionalAuthentication, HasModelPermission)
|
ConditionalAuthentication, HasModelPermission)
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
|
from . import forms
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import importers
|
from . import importers
|
||||||
|
@ -183,6 +189,40 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
f.audio_file.url)
|
f.audio_file.url)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@list_route(methods=['get'])
|
||||||
|
def viewable(self, request, *args, **kwargs):
|
||||||
|
return Response({}, status=200)
|
||||||
|
|
||||||
|
@list_route(methods=['get'])
|
||||||
|
def transcode(self, request, *args, **kwargs):
|
||||||
|
form = forms.TranscodeForm(request.GET)
|
||||||
|
if not form.is_valid():
|
||||||
|
return Response(form.errors, status=400)
|
||||||
|
|
||||||
|
f = form.cleaned_data['track_file']
|
||||||
|
output_kwargs = {
|
||||||
|
'format': form.cleaned_data['to']
|
||||||
|
}
|
||||||
|
args = (ffmpeg
|
||||||
|
.input(f.audio_file.path)
|
||||||
|
.output('pipe:', **output_kwargs)
|
||||||
|
.get_args()
|
||||||
|
)
|
||||||
|
# we use a generator here so the view return immediatly and send
|
||||||
|
# file chunk to the browser, instead of blocking a few seconds
|
||||||
|
def _transcode():
|
||||||
|
p = subprocess.Popen(
|
||||||
|
['ffmpeg'] + args,
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
for line in p.stdout:
|
||||||
|
yield line
|
||||||
|
|
||||||
|
response = StreamingHttpResponse(
|
||||||
|
_transcode(), status=200,
|
||||||
|
content_type=form.cleaned_data['to'])
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(viewsets.ReadOnlyModelViewSet):
|
class TagViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = Tag.objects.all().order_by('name')
|
queryset = Tag.objects.all().order_by('name')
|
||||||
|
|
|
@ -5,6 +5,7 @@ libjpeg-dev
|
||||||
zlib1g-dev
|
zlib1g-dev
|
||||||
libpq-dev
|
libpq-dev
|
||||||
postgresql-client
|
postgresql-client
|
||||||
libav-tools
|
libmagic-dev
|
||||||
|
ffmpeg
|
||||||
python3-dev
|
python3-dev
|
||||||
curl
|
curl
|
||||||
|
|
|
@ -57,3 +57,5 @@ git+https://github.com/EliotBerriot/django-cachalot.git@django-2
|
||||||
django-dynamic-preferences>=1.5,<1.6
|
django-dynamic-preferences>=1.5,<1.6
|
||||||
pyacoustid>=1.1.5,<1.2
|
pyacoustid>=1.1.5,<1.2
|
||||||
raven>=6.5,<7
|
raven>=6.5,<7
|
||||||
|
python-magic==0.4.15
|
||||||
|
ffmpeg-python==0.1.10
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from funkwhale_api.music import models
|
from funkwhale_api.music import models
|
||||||
from funkwhale_api.music import importers
|
from funkwhale_api.music import importers
|
||||||
from funkwhale_api.music import tasks
|
from funkwhale_api.music import tasks
|
||||||
|
|
||||||
|
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
def test_can_store_release_group_id_on_album(factories):
|
def test_can_store_release_group_id_on_album(factories):
|
||||||
album = factories['music.Album']()
|
album = factories['music.Album']()
|
||||||
|
@ -48,3 +51,15 @@ def test_import_job_is_bound_to_track_file(factories, mocker):
|
||||||
tasks.import_job_run(import_job_id=job.pk)
|
tasks.import_job_run(import_job_id=job.pk)
|
||||||
job.refresh_from_db()
|
job.refresh_from_db()
|
||||||
assert job.track_file.track == track
|
assert job.track_file.track == track
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('extention,mimetype', [
|
||||||
|
('ogg', 'audio/ogg'),
|
||||||
|
('mp3', 'audio/mpeg'),
|
||||||
|
])
|
||||||
|
def test_audio_track_mime_type(extention, mimetype, factories):
|
||||||
|
|
||||||
|
name = '.'.join(['test', extention])
|
||||||
|
path = os.path.join(DATA_DIR, name)
|
||||||
|
tf = factories['music.TrackFile'](audio_file__from_path=path)
|
||||||
|
|
||||||
|
assert tf.mimetype == mimetype
|
||||||
|
|
|
@ -39,6 +39,15 @@ server {
|
||||||
|
|
||||||
root /srv/funkwhale/front/dist;
|
root /srv/funkwhale/front/dist;
|
||||||
|
|
||||||
|
# global proxy conf
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_redirect off;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ @rewrites;
|
try_files $uri $uri/ @rewrites;
|
||||||
}
|
}
|
||||||
|
@ -49,15 +58,9 @@ server {
|
||||||
location /api/ {
|
location /api/ {
|
||||||
# this is needed if you have file import via upload enabled
|
# this is needed if you have file import via upload enabled
|
||||||
client_max_body_size 30M;
|
client_max_body_size 30M;
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
|
||||||
proxy_redirect off;
|
|
||||||
proxy_pass http://funkwhale-api/api/;
|
proxy_pass http://funkwhale-api/api/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /media/ {
|
location /media/ {
|
||||||
alias /srv/funkwhale/data/media/;
|
alias /srv/funkwhale/data/media/;
|
||||||
}
|
}
|
||||||
|
@ -70,6 +73,41 @@ server {
|
||||||
alias /srv/funkwhale/data/media;
|
alias /srv/funkwhale/data/media;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Transcoding logic and caching
|
||||||
|
location = /transcode-auth {
|
||||||
|
# needed so we can authenticate transcode requests, but still
|
||||||
|
# cache the result
|
||||||
|
internal;
|
||||||
|
set $query '';
|
||||||
|
# ensure we actually pass the jwt to the underlytin auth url
|
||||||
|
if ($request_uri ~* "[^\?]+\?(.*)$") {
|
||||||
|
set $query $1;
|
||||||
|
}
|
||||||
|
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_pass http://api:12081/api/v1/trackfiles/viewable/?$query;
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/v1/trackfiles/transcode/ {
|
||||||
|
# this block deals with authenticating and caching transcoding
|
||||||
|
# requests. Caching is heavily recommended as transcoding
|
||||||
|
# is a CPU intensive process.
|
||||||
|
auth_request /transcode-auth;
|
||||||
|
if ($args ~ (.*)jwt=[^&]*(.*)) {
|
||||||
|
set $cleaned_args $1$2;
|
||||||
|
}
|
||||||
|
proxy_cache_key "$scheme$request_method$host$uri$is_args$cleaned_args";
|
||||||
|
proxy_cache transcode;
|
||||||
|
proxy_cache_valid 200 7d;
|
||||||
|
proxy_ignore_headers "Set-Cookie";
|
||||||
|
proxy_hide_header "Set-Cookie";
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
proxy_pass http://funkwhale-api;
|
||||||
|
}
|
||||||
|
# end of transcoding logic
|
||||||
|
|
||||||
location /staticfiles/ {
|
location /staticfiles/ {
|
||||||
# django static files
|
# django static files
|
||||||
alias /srv/funkwhale/data/static/;
|
alias /srv/funkwhale/data/static/;
|
||||||
|
|
|
@ -26,23 +26,59 @@ http {
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
|
|
||||||
#gzip on;
|
#gzip on;
|
||||||
|
proxy_cache_path /tmp/funkwhale-transcode levels=1:2 keys_zone=transcode:10m max_size=1g inactive=24h use_temp_path=off;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 6001;
|
listen 6001;
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
client_max_body_size 20M;
|
client_max_body_size 20M;
|
||||||
|
|
||||||
|
# global proxy pass config
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host localhost:8080;
|
||||||
|
proxy_set_header X-Forwarded-Port 8080;
|
||||||
|
proxy_redirect off;
|
||||||
|
|
||||||
location /_protected/media {
|
location /_protected/media {
|
||||||
internal;
|
internal;
|
||||||
alias /protected/media;
|
alias /protected/media;
|
||||||
}
|
}
|
||||||
location / {
|
location = /transcode-auth {
|
||||||
proxy_set_header Host $host;
|
# needed so we can authenticate transcode requests, but still
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
# cache the result
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
internal;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
set $query '';
|
||||||
|
# ensure we actually pass the jwt to the underlytin auth url
|
||||||
|
if ($request_uri ~* "[^\?]+\?(.*)$") {
|
||||||
|
set $query $1;
|
||||||
|
}
|
||||||
proxy_set_header X-Forwarded-Host localhost:8080;
|
proxy_set_header X-Forwarded-Host localhost:8080;
|
||||||
proxy_set_header X-Forwarded-Port 8080;
|
proxy_set_header X-Forwarded-Port 8080;
|
||||||
proxy_redirect off;
|
proxy_pass http://api:12081/api/v1/trackfiles/viewable/?$query;
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/v1/trackfiles/transcode/ {
|
||||||
|
# this block deals with authenticating and caching transcoding
|
||||||
|
# requests. Caching is heavily recommended as transcoding
|
||||||
|
# is a CPU intensive process.
|
||||||
|
auth_request /transcode-auth;
|
||||||
|
if ($args ~ (.*)jwt=[^&]*(.*)) {
|
||||||
|
set $cleaned_args $1$2;
|
||||||
|
}
|
||||||
|
proxy_cache_key "$scheme$request_method$host$uri$is_args$cleaned_args";
|
||||||
|
proxy_cache transcode;
|
||||||
|
proxy_cache_valid 200 7d;
|
||||||
|
proxy_ignore_headers "Set-Cookie";
|
||||||
|
proxy_hide_header "Set-Cookie";
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
proxy_pass http://api:12081;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
proxy_pass http://api:12081/;
|
proxy_pass http://api:12081/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default {
|
||||||
|
formats: [
|
||||||
|
// 'audio/ogg',
|
||||||
|
'audio/mpeg'
|
||||||
|
],
|
||||||
|
formatsMap: {
|
||||||
|
'audio/ogg': 'ogg',
|
||||||
|
'audio/mpeg': 'mp3'
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="['ui', {'tiny': discrete}, 'buttons']">
|
<div :class="['ui', {'tiny': discrete}, 'buttons']">
|
||||||
<button title="Add to current queue" @click="add" :class="['ui', {'mini': discrete}, 'button']">
|
<button title="Add to current queue" @click="add" :class="['ui', {'mini': discrete}, {disabled: playableTracks.length === 0}, 'button']">
|
||||||
<i class="ui play icon"></i>
|
<i class="ui play icon"></i>
|
||||||
<template v-if="!discrete"><slot>Play</slot></template>
|
<template v-if="!discrete"><slot>Play</slot></template>
|
||||||
</button>
|
</button>
|
||||||
|
@ -36,20 +36,25 @@ export default {
|
||||||
jQuery(this.$el).find('.ui.dropdown').dropdown()
|
jQuery(this.$el).find('.ui.dropdown').dropdown()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
playableTracks () {
|
||||||
|
let tracks
|
||||||
|
if (this.track) {
|
||||||
|
tracks = [this.track]
|
||||||
|
} else {
|
||||||
|
tracks = this.tracks
|
||||||
|
}
|
||||||
|
return tracks.filter(e => {
|
||||||
|
return e.files.length > 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
add () {
|
add () {
|
||||||
if (this.track) {
|
this.$store.dispatch('queue/appendMany', {tracks: this.playableTracks})
|
||||||
this.$store.dispatch('queue/append', {track: this.track})
|
|
||||||
} else {
|
|
||||||
this.$store.dispatch('queue/appendMany', {tracks: this.tracks})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
addNext (next) {
|
addNext (next) {
|
||||||
if (this.track) {
|
this.$store.dispatch('queue/appendMany', {tracks: this.playableTracks, index: this.$store.state.queue.currentIndex + 1})
|
||||||
this.$store.dispatch('queue/append', {track: this.track, index: this.$store.state.queue.currentIndex + 1})
|
|
||||||
} else {
|
|
||||||
this.$store.dispatch('queue/appendMany', {tracks: this.tracks, index: this.$store.state.queue.currentIndex + 1})
|
|
||||||
}
|
|
||||||
if (next) {
|
if (next) {
|
||||||
this.$store.dispatch('queue/next')
|
this.$store.dispatch('queue/next')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<audio
|
<audio
|
||||||
ref="audio"
|
ref="audio"
|
||||||
:src="url"
|
|
||||||
@error="errored"
|
@error="errored"
|
||||||
@progress="updateLoad"
|
|
||||||
@loadeddata="loaded"
|
@loadeddata="loaded"
|
||||||
|
@durationchange="updateDuration"
|
||||||
@timeupdate="updateProgress"
|
@timeupdate="updateProgress"
|
||||||
@ended="ended"
|
@ended="ended"
|
||||||
preload>
|
preload>
|
||||||
|
<source v-for="src in srcs" :src="src.url" :type="src.type">
|
||||||
</audio>
|
</audio>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
import backend from '@/audio/backend'
|
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
|
import formats from '@/audio/formats'
|
||||||
|
|
||||||
// import logger from '@/logging'
|
// import logger from '@/logging'
|
||||||
|
|
||||||
|
@ -34,31 +33,43 @@ export default {
|
||||||
volume: state => state.player.volume,
|
volume: state => state.player.volume,
|
||||||
looping: state => state.player.looping
|
looping: state => state.player.looping
|
||||||
}),
|
}),
|
||||||
url: function () {
|
srcs: function () {
|
||||||
let file = this.track.files[0]
|
let file = this.track.files[0]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
this.$store.dispatch('player/trackErrored')
|
this.$store.dispatch('player/trackErrored')
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
let path = backend.absoluteUrl(file.path)
|
let sources = [
|
||||||
|
{type: file.mimetype, url: file.path}
|
||||||
|
]
|
||||||
|
formats.formats.forEach(f => {
|
||||||
|
if (f !== file.mimetype) {
|
||||||
|
let format = formats.formatsMap[f]
|
||||||
|
let url = `/api/v1/trackfiles/transcode/?track_file=${file.id}&to=${format}`
|
||||||
|
sources.push({type: f, url: url})
|
||||||
|
}
|
||||||
|
})
|
||||||
if (this.$store.state.auth.authenticated) {
|
if (this.$store.state.auth.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
|
||||||
// because for audio files we cannot use the regular Authentication
|
// because for audio files we cannot use the regular Authentication
|
||||||
// header
|
// header
|
||||||
path = url.updateQueryString(path, 'jwt', this.$store.state.auth.token)
|
sources.forEach(e => {
|
||||||
|
e.url = url.updateQueryString(e.url, 'jwt', this.$store.state.auth.token)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return path
|
return sources
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
errored: function () {
|
errored: function () {
|
||||||
this.$store.dispatch('player/trackErrored')
|
this.$store.dispatch('player/trackErrored')
|
||||||
},
|
},
|
||||||
updateLoad: function () {
|
updateDuration: function (e) {
|
||||||
|
this.$store.commit('player/duration', this.$refs.audio.duration)
|
||||||
},
|
},
|
||||||
loaded: function () {
|
loaded: function () {
|
||||||
|
this.$refs.audio.volume = this.volume
|
||||||
if (this.isCurrent) {
|
if (this.isCurrent) {
|
||||||
this.$store.commit('player/duration', this.$refs.audio.duration)
|
this.$store.commit('player/duration', this.$refs.audio.duration)
|
||||||
if (this.startTime) {
|
if (this.startTime) {
|
||||||
|
|
|
@ -50,7 +50,12 @@ export default {
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
durationFormatted: state => {
|
durationFormatted: state => {
|
||||||
return time.parse(Math.round(state.duration))
|
let duration = parseInt(state.duration)
|
||||||
|
if (duration % 1 !== 0) {
|
||||||
|
return time.parse(0)
|
||||||
|
}
|
||||||
|
duration = Math.round(state.duration)
|
||||||
|
return time.parse(duration)
|
||||||
},
|
},
|
||||||
currentTimeFormatted: state => {
|
currentTimeFormatted: state => {
|
||||||
return time.parse(Math.round(state.currentTime))
|
return time.parse(Math.round(state.currentTime))
|
||||||
|
|
Loading…
Reference in New Issue