From c857569162b7c36df100d90a160006154645970a Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Mon, 19 Jun 2017 00:08:48 +0200
Subject: [PATCH] Can now import OGG files from filesystem

---
 config/settings/common.py                     |  1 +
 funkwhale_api/music/metadata.py               | 11 +++-
 funkwhale_api/providers/audiofile/__init__.py |  4 ++
 funkwhale_api/providers/audiofile/importer.py | 60 ++++++++++++++++++
 .../audiofile/management/__init__.py          |  0
 .../audiofile/management/commands/__init__.py |  0
 .../management/commands/import_files.py       | 61 +++++++++++++++++++
 .../providers/audiofile/tests/__init__.py     |  0
 .../providers/audiofile/tests/dummy_file.ogg  |  0
 .../audiofile/tests/test_disk_import.py       | 34 +++++++++++
 10 files changed, 169 insertions(+), 2 deletions(-)
 create mode 100644 funkwhale_api/providers/audiofile/__init__.py
 create mode 100644 funkwhale_api/providers/audiofile/importer.py
 create mode 100644 funkwhale_api/providers/audiofile/management/__init__.py
 create mode 100644 funkwhale_api/providers/audiofile/management/commands/__init__.py
 create mode 100644 funkwhale_api/providers/audiofile/management/commands/import_files.py
 create mode 100644 funkwhale_api/providers/audiofile/tests/__init__.py
 create mode 100644 funkwhale_api/providers/audiofile/tests/dummy_file.ogg
 create mode 100644 funkwhale_api/providers/audiofile/tests/test_disk_import.py

diff --git a/config/settings/common.py b/config/settings/common.py
index 6efad40..ceee21f 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 92914ca..3fe61e6 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 0000000..18e2c46
--- /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 0000000..d95c120
--- /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 0000000..e69de29
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 0000000..e69de29
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 0000000..8a99156
--- /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 0000000..e69de29
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 0000000..e69de29
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 0000000..26532a8
--- /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])
-- 
GitLab