From 1bc4ceab9e52c446ffedeb119da31c0dd451f322 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 15 May 2018 21:59:29 +0200 Subject: [PATCH] See #195: set bitrate, duration and size when importing file --- api/funkwhale_api/music/factories.py | 4 ++++ api/funkwhale_api/music/models.py | 18 ++++++++++++++++++ api/funkwhale_api/music/serializers.py | 15 +++++++++++++-- api/funkwhale_api/music/tasks.py | 13 +++++++++++++ api/funkwhale_api/music/utils.py | 10 ++++++++++ api/tests/music/test_import.py | 23 +++++++++++++++++++---- api/tests/music/test_models.py | 25 +++++++++++++++++++++++++ api/tests/music/test_tasks.py | 7 ++++++- api/tests/music/test_utils.py | 18 ++++++++++++++++++ 9 files changed, 126 insertions(+), 7 deletions(-) diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index 1df949904..412e2f798 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -54,6 +54,10 @@ class TrackFileFactory(factory.django.DjangoModelFactory): audio_file = factory.django.FileField( from_path=os.path.join(SAMPLES_PATH, 'test.ogg')) + bitrate = None + size = None + duration = None + class Meta: model = 'music.TrackFile' diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 0ab852890..7c5fbe9a6 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -479,6 +479,24 @@ class TrackFile(models.Model): return 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): if not self.mimetype and self.audio_file: self.mimetype = utils.guess_mimetype(self.audio_file) diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 9dfc91478..d9d48496e 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -27,6 +27,7 @@ class SimpleArtistSerializer(serializers.ModelSerializer): class ArtistSerializer(serializers.ModelSerializer): tags = TagSerializer(many=True, read_only=True) + class Meta: model = models.Artist fields = ('id', 'mbid', 'name', 'tags', 'creation_date') @@ -40,11 +41,21 @@ class TrackFileSerializer(serializers.ModelSerializer): fields = ( 'id', 'path', - 'duration', 'source', 'filename', 'mimetype', - 'track') + 'track', + 'duration', + 'mimetype', + 'bitrate', + 'size', + ) + read_only_fields = [ + 'duration', + 'mimetype', + 'bitrate', + 'size', + ] def get_path(self, o): url = o.path diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index bad0006aa..a6d76b962 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -134,6 +134,19 @@ def _do_import(import_job, replace=False, use_acoustid=True): # in place import, we set mimetype from extension path, ext = os.path.splitext(import_job.source) 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() import_job.status = 'finished' import_job.track_file = track_file diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index 0c73f408f..04ca208d6 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -1,5 +1,6 @@ import magic import mimetypes +import mutagen import re from django.db.models import Q @@ -82,3 +83,12 @@ def get_type_from_ext(extension): # we remove leading dot extension = extension[1:] 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 diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py index c7b40fb16..8453dca84 100644 --- a/api/tests/music/test_import.py +++ b/api/tests/music/test_import.py @@ -1,4 +1,5 @@ import json +import os import pytest from django.urls import reverse @@ -7,6 +8,8 @@ from funkwhale_api.federation import actors from funkwhale_api.federation import serializers as federation_serializers from funkwhale_api.music import tasks +DATA_DIR = os.path.dirname(os.path.abspath(__file__)) + def test_create_import_can_bind_to_request( artists, albums, mocker, factories, superuser_api_client): @@ -40,11 +43,20 @@ def test_create_import_can_bind_to_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']( artist_name='Hello', album_title='World', title='Ping', + metadata__length=42, + metadata__bitrate=43, + metadata__size=44, ) job = factories['music.ImportJob']( federation=True, @@ -56,6 +68,9 @@ def test_import_job_from_federation_no_musicbrainz(factories): tf = job.track_file 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.track.title == 'Ping' assert tf.track.artist.name == 'Hello' @@ -234,13 +249,13 @@ def test_import_batch_notifies_followers( def test__do_import_in_place_mbid(factories, tmpfile): - path = '/test.ogg' + path = os.path.join(DATA_DIR, 'test.ogg') 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) tf = tasks._do_import(job, use_acoustid=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' diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py index d76c09a01..e926d07fa 100644 --- a/api/tests/music/test_models.py +++ b/api/tests/music/test_models.py @@ -85,3 +85,28 @@ def test_track_file_file_name(factories): tf = factories['music.TrackFile'](audio_file__from_path=path) 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 diff --git a/api/tests/music/test_tasks.py b/api/tests/music/test_tasks.py index ddbc4ba9a..c5839432b 100644 --- a/api/tests/music/test_tasks.py +++ b/api/tests/music/test_tasks.py @@ -62,6 +62,9 @@ def test_import_job_can_run_with_file_and_acoustid( 'score': 0.860825}], 'status': 'ok' } + mocker.patch( + 'funkwhale_api.music.utils.get_audio_file_data', + return_value={'bitrate': 42, 'length': 43}) mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', return_value=artists['get']['adhesive_wombat']) @@ -82,7 +85,9 @@ def test_import_job_can_run_with_file_and_acoustid( with open(path, 'rb') as f: 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 assert not job.audio_file assert job.status == 'finished' diff --git a/api/tests/music/test_utils.py b/api/tests/music/test_utils.py index 0a4f4b994..12b381a99 100644 --- a/api/tests/music/test_utils.py +++ b/api/tests/music/test_utils.py @@ -1,5 +1,10 @@ +import os +import pytest + from funkwhale_api.music import utils +DATA_DIR = os.path.dirname(os.path.abspath(__file__)) + def test_guess_mimetype_try_using_extension(factories, mocker): mocker.patch( @@ -17,3 +22,16 @@ def test_guess_mimetype_try_using_extension_if_fail(factories, mocker): audio_file__filename='test.mp3') 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