diff --git a/config/settings/common.py b/config/settings/common.py index 6efad404cd7b832892e9643263645b2595ae3ee0..ceee21f9467a615b976ced99fa742518ca698000 100644 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -64,6 +64,7 @@ LOCAL_APPS = ( 'funkwhale_api.radios', 'funkwhale_api.history', 'funkwhale_api.playlists', + 'funkwhale_api.providers.audiofile', ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps diff --git a/funkwhale_api/music/metadata.py b/funkwhale_api/music/metadata.py index 92914ca22413013edabed1027d88bfd7d2a614de..3fe61e652b085eb404c9c6753475d9a079507aed 100644 --- a/funkwhale_api/music/metadata.py +++ b/funkwhale_api/music/metadata.py @@ -1,5 +1,6 @@ import mutagen +NODEFAULT = object() class Metadata(object): ALIASES = { @@ -11,8 +12,14 @@ class Metadata(object): def __init__(self, path): self._file = mutagen.File(path) - def get(self, key, single=True): - v = self._file[key] + def get(self, key, default=NODEFAULT, single=True): + try: + v = self._file[key] + except KeyError: + if default == NODEFAULT: + raise + return default + # Some tags are returned as lists of string if single: return v[0] diff --git a/funkwhale_api/providers/audiofile/__init__.py b/funkwhale_api/providers/audiofile/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..18e2c469a0aa29a634080d4efe1e3959650d4601 --- /dev/null +++ b/funkwhale_api/providers/audiofile/__init__.py @@ -0,0 +1,4 @@ +""" +This module is responsible from importing existing audiofiles from the +filesystem into funkwhale. +""" diff --git a/funkwhale_api/providers/audiofile/importer.py b/funkwhale_api/providers/audiofile/importer.py new file mode 100644 index 0000000000000000000000000000000000000000..d95c120e1f7e8eec9faacc14e4b010491d321c1c --- /dev/null +++ b/funkwhale_api/providers/audiofile/importer.py @@ -0,0 +1,60 @@ +import os +import datetime +from django.core.files import File + +from funkwhale_api.taskapp import celery +from funkwhale_api.music import models, metadata + + +@celery.app.task(name='audiofile.from_path') +def from_path(path): + data = metadata.Metadata(path) + + artist = models.Artist.objects.get_or_create( + name__iexact=data.get('artist'), + defaults={'name': data.get('artist')}, + )[0] + + release_date = None + try: + year, month, day = data.get('date', None).split('-') + release_date = datetime.date( + int(year), int(month), int(day) + ) + except (ValueError, TypeError): + pass + + album = models.Album.objects.get_or_create( + title__iexact=data.get('album'), + artist=artist, + defaults={ + 'title': data.get('album'), + 'release_date': release_date, + }, + )[0] + + position = None + try: + position = int(data.get('tracknumber', None)) + except ValueError: + pass + track = models.Track.objects.get_or_create( + title__iexact=data.get('title'), + album=album, + defaults={ + 'title': data.get('title'), + 'position': position, + }, + )[0] + + if track.files.count() > 0: + raise ValueError('File already exists for track {}'.format(track.pk)) + + track_file = models.TrackFile(track=track) + track_file.audio_file.save( + os.path.basename(path), + File(open(path, 'rb')) + ) + track_file.save() + + return track_file diff --git a/funkwhale_api/providers/audiofile/management/__init__.py b/funkwhale_api/providers/audiofile/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/funkwhale_api/providers/audiofile/management/commands/__init__.py b/funkwhale_api/providers/audiofile/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/funkwhale_api/providers/audiofile/management/commands/import_files.py b/funkwhale_api/providers/audiofile/management/commands/import_files.py new file mode 100644 index 0000000000000000000000000000000000000000..8a99156cb0c477bd371910ef1bbb0a193a2bf52b --- /dev/null +++ b/funkwhale_api/providers/audiofile/management/commands/import_files.py @@ -0,0 +1,61 @@ +import glob +from django.core.management.base import BaseCommand, CommandError +from funkwhale_api.providers.audiofile import importer + + +class Command(BaseCommand): + help = 'Import audio files mathinc given glob pattern' + + def add_arguments(self, parser): + parser.add_argument('path', type=str) + parser.add_argument( + '--recursive', + action='store_true', + dest='recursive', + default=False, + help='Will match the pattern recursively (including subdirectories)', + ) + parser.add_argument( + '--async', + action='store_true', + dest='async', + default=False, + help='Will launch celery tasks for each file to import instead of doing it synchronously and block the CLI', + ) + parser.add_argument( + '--noinput', '--no-input', action='store_false', dest='interactive', + help="Do NOT prompt the user for input of any kind.", + ) + + def handle(self, *args, **options): + # self.stdout.write(self.style.SUCCESS('Successfully closed poll "%s"' % poll_id)) + matching = glob.glob(options['path'], recursive=options['recursive']) + self.stdout.write('This will import {} files matching this pattern: {}'.format( + len(matching), options['path'])) + + if not matching: + raise CommandError('No file matching pattern, aborting') + + if options['interactive']: + message = ( + 'Are you sure you want to do this?\n\n' + "Type 'yes' to continue, or 'no' to cancel: " + ) + if input(''.join(message)) != 'yes': + raise CommandError("Import cancelled.") + + message = 'Importing {}...' + if options['async']: + message = 'Launching import for {}...' + + for path in matching: + self.stdout.write(message.format(path)) + try: + importer.from_path(path) + except Exception as e: + self.stdout.write('Error: {}'.format(e)) + + message = 'Successfully imported {} tracks' + if options['async']: + message = 'Successfully launched import for {} tracks' + self.stdout.write(message.format(len(matching))) diff --git a/funkwhale_api/providers/audiofile/tests/__init__.py b/funkwhale_api/providers/audiofile/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/funkwhale_api/providers/audiofile/tests/dummy_file.ogg b/funkwhale_api/providers/audiofile/tests/dummy_file.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/funkwhale_api/providers/audiofile/tests/test_disk_import.py b/funkwhale_api/providers/audiofile/tests/test_disk_import.py new file mode 100644 index 0000000000000000000000000000000000000000..26532a8c93aba087d0dcd07fedaedfe9b47e23c1 --- /dev/null +++ b/funkwhale_api/providers/audiofile/tests/test_disk_import.py @@ -0,0 +1,34 @@ +import os +import datetime +import unittest +from test_plus.test import TestCase + +from funkwhale_api.providers.audiofile import importer + +DATA_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class TestAudioFile(TestCase): + def test_can_import_single_audio_file(self, *mocks): + metadata = { + 'artist': ['Test artist'], + 'album': ['Test album'], + 'title': ['Test track'], + 'tracknumber': ['4'], + 'date': ['2012-08-15'] + } + + with unittest.mock.patch('mutagen.File', return_value=metadata): + track_file = importer.from_path( + os.path.join(DATA_DIR, 'dummy_file.ogg')) + + self.assertEqual( + track_file.track.title, metadata['title'][0]) + self.assertEqual( + track_file.track.position, 4) + self.assertEqual( + track_file.track.album.title, metadata['album'][0]) + self.assertEqual( + track_file.track.album.release_date, datetime.date(2012, 8, 15)) + self.assertEqual( + track_file.track.artist.name, metadata['artist'][0])