kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Merge branch 'transcoding' into 'develop'
Transcoding Closes #60 See merge request funkwhale/funkwhale!47merge-requests/154/head
commit
953d0ddc91
|
@ -72,7 +72,7 @@ api/music
|
|||
api/media
|
||||
api/staticfiles
|
||||
api/static
|
||||
|
||||
api/.pytest_cache
|
||||
|
||||
# Front
|
||||
front/node_modules/
|
||||
|
|
14
CHANGELOG
14
CHANGELOG
|
@ -6,6 +6,20 @@ Changelog
|
|||
----------------
|
||||
|
||||
- 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)
|
||||
----------------
|
||||
|
|
|
@ -3,7 +3,7 @@ FROM python:3.5
|
|||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# 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
|
||||
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
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
FROM python:3.5
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
|
||||
# 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 ./install_os_dependencies.sh /install_os_dependencies.sh
|
||||
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 musicbrainz
|
||||
from . import importers
|
||||
from . import utils
|
||||
|
||||
|
||||
class APIModelMixin(models.Model):
|
||||
|
@ -364,6 +365,7 @@ class TrackFile(models.Model):
|
|||
source = models.URLField(null=True, blank=True)
|
||||
duration = models.IntegerField(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):
|
||||
# import the track file, since there is not any
|
||||
|
@ -393,6 +395,10 @@ class TrackFile(models.Model):
|
|||
self.track.full_name,
|
||||
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):
|
||||
IMPORT_BATCH_SOURCES = [
|
||||
|
|
|
@ -28,7 +28,14 @@ class TrackFileSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = models.TrackFile
|
||||
fields = ('id', 'path', 'duration', 'source', 'filename', 'track')
|
||||
fields = (
|
||||
'id',
|
||||
'path',
|
||||
'duration',
|
||||
'source',
|
||||
'filename',
|
||||
'mimetype',
|
||||
'track')
|
||||
|
||||
def get_path(self, o):
|
||||
url = o.path
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import magic
|
||||
import re
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
def normalize_query(query_string,
|
||||
findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
|
||||
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)]
|
||||
|
||||
|
||||
def get_query(query_string, search_fields):
|
||||
''' 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.
|
||||
|
@ -35,3 +38,8 @@ def get_query(query_string, search_fields):
|
|||
else:
|
||||
query = query & or_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 json
|
||||
import subprocess
|
||||
import unicodedata
|
||||
import urllib
|
||||
|
||||
from django.urls import reverse
|
||||
from django.db import models, transaction
|
||||
from django.db.models.functions import Length
|
||||
from django.conf import settings
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
from rest_framework import viewsets, views, mixins
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.response import Response
|
||||
|
@ -19,6 +24,7 @@ from funkwhale_api.common.permissions import (
|
|||
ConditionalAuthentication, HasModelPermission)
|
||||
from taggit.models import Tag
|
||||
|
||||
from . import forms
|
||||
from . import models
|
||||
from . import serializers
|
||||
from . import importers
|
||||
|
@ -183,6 +189,40 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
f.audio_file.url)
|
||||
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):
|
||||
queryset = Tag.objects.all().order_by('name')
|
||||
|
|
|
@ -5,6 +5,7 @@ libjpeg-dev
|
|||
zlib1g-dev
|
||||
libpq-dev
|
||||
postgresql-client
|
||||
libav-tools
|
||||
libmagic-dev
|
||||
ffmpeg
|
||||
python3-dev
|
||||
curl
|
||||
|
|
|
@ -57,3 +57,5 @@ git+https://github.com/EliotBerriot/django-cachalot.git@django-2
|
|||
django-dynamic-preferences>=1.5,<1.6
|
||||
pyacoustid>=1.1.5,<1.2
|
||||
raven>=6.5,<7
|
||||
python-magic==0.4.15
|
||||
ffmpeg-python==0.1.10
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import os
|
||||
import pytest
|
||||
|
||||
from funkwhale_api.music import models
|
||||
from funkwhale_api.music import importers
|
||||
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):
|
||||
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)
|
||||
job.refresh_from_db()
|
||||
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;
|
||||
|
||||
# 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 / {
|
||||
try_files $uri $uri/ @rewrites;
|
||||
}
|
||||
|
@ -49,15 +58,9 @@ server {
|
|||
location /api/ {
|
||||
# this is needed if you have file import via upload enabled
|
||||
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/;
|
||||
}
|
||||
|
||||
location /media/ {
|
||||
alias /srv/funkwhale/data/media/;
|
||||
}
|
||||
|
@ -70,6 +73,41 @@ server {
|
|||
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/ {
|
||||
# django static files
|
||||
alias /srv/funkwhale/data/static/;
|
||||
|
|
|
@ -26,23 +26,59 @@ http {
|
|||
keepalive_timeout 65;
|
||||
|
||||
#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 {
|
||||
listen 6001;
|
||||
charset utf-8;
|
||||
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 {
|
||||
internal;
|
||||
alias /protected/media;
|
||||
}
|
||||
location / {
|
||||
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;
|
||||
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 localhost: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/;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
formats: [
|
||||
// 'audio/ogg',
|
||||
'audio/mpeg'
|
||||
],
|
||||
formatsMap: {
|
||||
'audio/ogg': 'ogg',
|
||||
'audio/mpeg': 'mp3'
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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>
|
||||
<template v-if="!discrete"><slot>Play</slot></template>
|
||||
</button>
|
||||
|
@ -36,20 +36,25 @@ export default {
|
|||
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: {
|
||||
add () {
|
||||
if (this.track) {
|
||||
this.$store.dispatch('queue/append', {track: this.track})
|
||||
} else {
|
||||
this.$store.dispatch('queue/appendMany', {tracks: this.tracks})
|
||||
}
|
||||
this.$store.dispatch('queue/appendMany', {tracks: this.playableTracks})
|
||||
},
|
||||
addNext (next) {
|
||||
if (this.track) {
|
||||
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})
|
||||
}
|
||||
this.$store.dispatch('queue/appendMany', {tracks: this.playableTracks, index: this.$store.state.queue.currentIndex + 1})
|
||||
if (next) {
|
||||
this.$store.dispatch('queue/next')
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
<template>
|
||||
<audio
|
||||
ref="audio"
|
||||
:src="url"
|
||||
@error="errored"
|
||||
@progress="updateLoad"
|
||||
@loadeddata="loaded"
|
||||
@durationchange="updateDuration"
|
||||
@timeupdate="updateProgress"
|
||||
@ended="ended"
|
||||
preload>
|
||||
|
||||
<source v-for="src in srcs" :src="src.url" :type="src.type">
|
||||
</audio>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import backend from '@/audio/backend'
|
||||
import url from '@/utils/url'
|
||||
import formats from '@/audio/formats'
|
||||
|
||||
// import logger from '@/logging'
|
||||
|
||||
|
@ -34,31 +33,43 @@ export default {
|
|||
volume: state => state.player.volume,
|
||||
looping: state => state.player.looping
|
||||
}),
|
||||
url: function () {
|
||||
srcs: function () {
|
||||
let file = this.track.files[0]
|
||||
if (!file) {
|
||||
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) {
|
||||
// we need to send the token directly in url
|
||||
// so authentication can be checked by the backend
|
||||
// because for audio files we cannot use the regular Authentication
|
||||
// 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: {
|
||||
errored: function () {
|
||||
this.$store.dispatch('player/trackErrored')
|
||||
},
|
||||
updateLoad: function () {
|
||||
|
||||
updateDuration: function (e) {
|
||||
this.$store.commit('player/duration', this.$refs.audio.duration)
|
||||
},
|
||||
loaded: function () {
|
||||
this.$refs.audio.volume = this.volume
|
||||
if (this.isCurrent) {
|
||||
this.$store.commit('player/duration', this.$refs.audio.duration)
|
||||
if (this.startTime) {
|
||||
|
|
|
@ -50,7 +50,12 @@ export default {
|
|||
},
|
||||
getters: {
|
||||
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 => {
|
||||
return time.parse(Math.round(state.currentTime))
|
||||
|
|
Ładowanie…
Reference in New Issue