Skip to content
Snippets Groups Projects
Verified Commit 1bc4ceab authored by Eliot Berriot's avatar Eliot Berriot
Browse files

See #195: set bitrate, duration and size when importing file

parent 7425a8ea
No related branches found
No related tags found
No related merge requests found
...@@ -54,6 +54,10 @@ class TrackFileFactory(factory.django.DjangoModelFactory): ...@@ -54,6 +54,10 @@ class TrackFileFactory(factory.django.DjangoModelFactory):
audio_file = factory.django.FileField( audio_file = factory.django.FileField(
from_path=os.path.join(SAMPLES_PATH, 'test.ogg')) from_path=os.path.join(SAMPLES_PATH, 'test.ogg'))
bitrate = None
size = None
duration = None
class Meta: class Meta:
model = 'music.TrackFile' model = 'music.TrackFile'
......
...@@ -479,6 +479,24 @@ class TrackFile(models.Model): ...@@ -479,6 +479,24 @@ class TrackFile(models.Model):
return return
return os.path.splitext(self.audio_file.name)[-1].replace('.', '', 1) return os.path.splitext(self.audio_file.name)[-1].replace('.', '', 1)
def get_file_size(self):
if self.audio_file:
return self.audio_file.size
if self.source.startswith('file://'):
return os.path.getsize(self.source.replace('file://', '', 1))
if self.library_track and self.library_track.audio_file:
return self.library_track.audio_file.size
def get_audio_file(self):
if self.audio_file:
return self.audio_file.open()
if self.source.startswith('file://'):
return open(self.source.replace('file://', '', 1), 'rb')
if self.library_track and self.library_track.audio_file:
return self.library_track.audio_file.open()
def save(self, **kwargs): def save(self, **kwargs):
if not self.mimetype and self.audio_file: if not self.mimetype and self.audio_file:
self.mimetype = utils.guess_mimetype(self.audio_file) self.mimetype = utils.guess_mimetype(self.audio_file)
......
...@@ -27,6 +27,7 @@ class SimpleArtistSerializer(serializers.ModelSerializer): ...@@ -27,6 +27,7 @@ class SimpleArtistSerializer(serializers.ModelSerializer):
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', 'creation_date') fields = ('id', 'mbid', 'name', 'tags', 'creation_date')
...@@ -40,11 +41,21 @@ class TrackFileSerializer(serializers.ModelSerializer): ...@@ -40,11 +41,21 @@ class TrackFileSerializer(serializers.ModelSerializer):
fields = ( fields = (
'id', 'id',
'path', 'path',
'duration',
'source', 'source',
'filename', 'filename',
'mimetype', 'mimetype',
'track') 'track',
'duration',
'mimetype',
'bitrate',
'size',
)
read_only_fields = [
'duration',
'mimetype',
'bitrate',
'size',
]
def get_path(self, o): def get_path(self, o):
url = o.path url = o.path
......
...@@ -134,6 +134,19 @@ def _do_import(import_job, replace=False, use_acoustid=True): ...@@ -134,6 +134,19 @@ def _do_import(import_job, replace=False, use_acoustid=True):
# in place import, we set mimetype from extension # in place import, we set mimetype from extension
path, ext = os.path.splitext(import_job.source) path, ext = os.path.splitext(import_job.source)
track_file.mimetype = music_utils.get_type_from_ext(ext) track_file.mimetype = music_utils.get_type_from_ext(ext)
audio_file = track_file.get_audio_file()
if audio_file:
with audio_file as f:
audio_data = music_utils.get_audio_file_data(f)
track_file.duration = int(audio_data['length'])
track_file.bitrate = audio_data['bitrate']
track_file.size = track_file.get_file_size()
else:
lt = track_file.library_track
if lt:
track_file.duration = lt.get_metadata('length')
track_file.size = lt.get_metadata('size')
track_file.bitrate = lt.get_metadata('bitrate')
track_file.save() track_file.save()
import_job.status = 'finished' import_job.status = 'finished'
import_job.track_file = track_file import_job.track_file = track_file
......
import magic import magic
import mimetypes import mimetypes
import mutagen
import re import re
from django.db.models import Q from django.db.models import Q
...@@ -82,3 +83,12 @@ def get_type_from_ext(extension): ...@@ -82,3 +83,12 @@ def get_type_from_ext(extension):
# we remove leading dot # we remove leading dot
extension = extension[1:] extension = extension[1:]
return EXTENSION_TO_MIMETYPE.get(extension) return EXTENSION_TO_MIMETYPE.get(extension)
def get_audio_file_data(f):
data = mutagen.File(f)
d = {}
d['bitrate'] = data.info.bitrate
d['length'] = data.info.length
return d
import json import json
import os
import pytest import pytest
from django.urls import reverse from django.urls import reverse
...@@ -7,6 +8,8 @@ from funkwhale_api.federation import actors ...@@ -7,6 +8,8 @@ from funkwhale_api.federation import actors
from funkwhale_api.federation import serializers as federation_serializers from funkwhale_api.federation import serializers as federation_serializers
from funkwhale_api.music import tasks from funkwhale_api.music import tasks
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
def test_create_import_can_bind_to_request( def test_create_import_can_bind_to_request(
artists, albums, mocker, factories, superuser_api_client): artists, albums, mocker, factories, superuser_api_client):
...@@ -40,11 +43,20 @@ def test_create_import_can_bind_to_request( ...@@ -40,11 +43,20 @@ def test_create_import_can_bind_to_request(
assert batch.import_request == request assert batch.import_request == request
def test_import_job_from_federation_no_musicbrainz(factories): def test_import_job_from_federation_no_musicbrainz(factories, mocker):
mocker.patch(
'funkwhale_api.music.utils.get_audio_file_data',
return_value={'bitrate': 24, 'length': 666})
mocker.patch(
'funkwhale_api.music.models.TrackFile.get_file_size',
return_value=42)
lt = factories['federation.LibraryTrack']( lt = factories['federation.LibraryTrack'](
artist_name='Hello', artist_name='Hello',
album_title='World', album_title='World',
title='Ping', title='Ping',
metadata__length=42,
metadata__bitrate=43,
metadata__size=44,
) )
job = factories['music.ImportJob']( job = factories['music.ImportJob'](
federation=True, federation=True,
...@@ -56,6 +68,9 @@ def test_import_job_from_federation_no_musicbrainz(factories): ...@@ -56,6 +68,9 @@ def test_import_job_from_federation_no_musicbrainz(factories):
tf = job.track_file tf = job.track_file
assert tf.mimetype == lt.audio_mimetype assert tf.mimetype == lt.audio_mimetype
assert tf.duration == 42
assert tf.bitrate == 43
assert tf.size == 44
assert tf.library_track == job.library_track assert tf.library_track == job.library_track
assert tf.track.title == 'Ping' assert tf.track.title == 'Ping'
assert tf.track.artist.name == 'Hello' assert tf.track.artist.name == 'Hello'
...@@ -234,13 +249,13 @@ def test_import_batch_notifies_followers( ...@@ -234,13 +249,13 @@ def test_import_batch_notifies_followers(
def test__do_import_in_place_mbid(factories, tmpfile): def test__do_import_in_place_mbid(factories, tmpfile):
path = '/test.ogg' path = os.path.join(DATA_DIR, 'test.ogg')
job = factories['music.ImportJob']( job = factories['music.ImportJob'](
in_place=True, source='file:///test.ogg') in_place=True, source='file://{}'.format(path))
track = factories['music.Track'](mbid=job.mbid) track = factories['music.Track'](mbid=job.mbid)
tf = tasks._do_import(job, use_acoustid=False) tf = tasks._do_import(job, use_acoustid=False)
assert bool(tf.audio_file) is False assert bool(tf.audio_file) is False
assert tf.source == 'file:///test.ogg' assert tf.source == 'file://{}'.format(path)
assert tf.mimetype == 'audio/ogg' assert tf.mimetype == 'audio/ogg'
...@@ -85,3 +85,28 @@ def test_track_file_file_name(factories): ...@@ -85,3 +85,28 @@ def test_track_file_file_name(factories):
tf = factories['music.TrackFile'](audio_file__from_path=path) tf = factories['music.TrackFile'](audio_file__from_path=path)
assert tf.filename == tf.track.full_name + '.mp3' assert tf.filename == tf.track.full_name + '.mp3'
def test_track_get_file_size(factories):
name = 'test.mp3'
path = os.path.join(DATA_DIR, name)
tf = factories['music.TrackFile'](audio_file__from_path=path)
assert tf.get_file_size() == 297745
def test_track_get_file_size_federation(factories):
tf = factories['music.TrackFile'](
federation=True,
library_track__with_audio_file=True)
assert tf.get_file_size() == tf.library_track.audio_file.size
def test_track_get_file_size_in_place(factories):
name = 'test.mp3'
path = os.path.join(DATA_DIR, name)
tf = factories['music.TrackFile'](
in_place=True, source='file://{}'.format(path))
assert tf.get_file_size() == 297745
...@@ -62,6 +62,9 @@ def test_import_job_can_run_with_file_and_acoustid( ...@@ -62,6 +62,9 @@ def test_import_job_can_run_with_file_and_acoustid(
'score': 0.860825}], 'score': 0.860825}],
'status': 'ok' 'status': 'ok'
} }
mocker.patch(
'funkwhale_api.music.utils.get_audio_file_data',
return_value={'bitrate': 42, 'length': 43})
mocker.patch( mocker.patch(
'funkwhale_api.musicbrainz.api.artists.get', 'funkwhale_api.musicbrainz.api.artists.get',
return_value=artists['get']['adhesive_wombat']) return_value=artists['get']['adhesive_wombat'])
...@@ -82,7 +85,9 @@ def test_import_job_can_run_with_file_and_acoustid( ...@@ -82,7 +85,9 @@ def test_import_job_can_run_with_file_and_acoustid(
with open(path, 'rb') as f: with open(path, 'rb') as f:
assert track_file.audio_file.read() == f.read() assert track_file.audio_file.read() == f.read()
assert track_file.duration == 268 assert track_file.bitrate == 42
assert track_file.duration == 43
assert track_file.size == os.path.getsize(path)
# audio file is deleted from import job once persisted to audio file # audio file is deleted from import job once persisted to audio file
assert not job.audio_file assert not job.audio_file
assert job.status == 'finished' assert job.status == 'finished'
......
import os
import pytest
from funkwhale_api.music import utils from funkwhale_api.music import utils
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
def test_guess_mimetype_try_using_extension(factories, mocker): def test_guess_mimetype_try_using_extension(factories, mocker):
mocker.patch( mocker.patch(
...@@ -17,3 +22,16 @@ def test_guess_mimetype_try_using_extension_if_fail(factories, mocker): ...@@ -17,3 +22,16 @@ def test_guess_mimetype_try_using_extension_if_fail(factories, mocker):
audio_file__filename='test.mp3') audio_file__filename='test.mp3')
assert utils.guess_mimetype(f.audio_file) == 'audio/mpeg' assert utils.guess_mimetype(f.audio_file) == 'audio/mpeg'
@pytest.mark.parametrize('name, expected', [
('sample.flac', {'bitrate': 1608000, 'length': 0.001}),
('test.mp3', {'bitrate': 8000, 'length': 267.70285714285717}),
('test.ogg', {'bitrate': 128000, 'length': 229.18304166666667}),
])
def test_get_audio_file_data(name, expected):
path = os.path.join(DATA_DIR, name)
with open(path, 'rb') as f:
result = utils.get_audio_file_data(f)
assert result == expected
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment