Initial commit that merge both the front end and the API in the same repository

This commit is contained in:
Eliot Berriot 2017-06-23 23:00:42 +02:00
commit 76f98b74dd
285 changed files with 51318 additions and 0 deletions

View file

View file

@ -0,0 +1,4 @@
"""
This module is responsible from importing existing audiofiles from the
filesystem into funkwhale.
"""

View file

@ -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

View file

@ -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)))

View file

@ -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])

View file

@ -0,0 +1,8 @@
from django.conf.urls import include, url
from funkwhale_api.music import views
urlpatterns = [
url(r'^youtube/', include('funkwhale_api.providers.youtube.urls', namespace='youtube')),
url(r'^musicbrainz/', include('funkwhale_api.musicbrainz.urls', namespace='musicbrainz')),
]

View file

@ -0,0 +1,58 @@
import threading
from apiclient.discovery import build
from apiclient.errors import HttpError
from oauth2client.tools import argparser
from django.conf import settings
# Set DEVELOPER_KEY to the API key value from the APIs & auth > Registered apps
# tab of
# https://cloud.google.com/console
# Please ensure that you have enabled the YouTube Data API for your project.
DEVELOPER_KEY = settings.FUNKWHALE_PROVIDERS['youtube']['api_key']
YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"
VIDEO_BASE_URL = 'https://www.youtube.com/watch?v={0}'
def _do_search(query):
youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
developerKey=DEVELOPER_KEY)
return youtube.search().list(
q=query,
part="id,snippet",
maxResults=25
).execute()
class Client(object):
def search(self, query):
search_response = _do_search(query)
videos = []
for search_result in search_response.get("items", []):
if search_result["id"]["kind"] == "youtube#video":
search_result['full_url'] = VIDEO_BASE_URL.format(search_result["id"]['videoId'])
videos.append(search_result)
return videos
def search_multiple(self, queries):
results = {}
def search(key, query):
results[key] = self.search(query)
threads = [
threading.Thread(target=search, args=(key, query,))
for key, query in queries.items()
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
return results
client = Client()

View file

@ -0,0 +1,162 @@
search = {}
search['8 bit adventure'] = {
"pageInfo": {
"totalResults": 1000000,
"resultsPerPage": 25
},
"nextPageToken": "CBkQAA",
"etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/1L34zetsKWv-raAFiz0MuT0SsfQ\"",
"items": [
{
"id": {
"videoId": "0HxZn6CzOIo",
"kind": "youtube#video"
},
"etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/GxK-wHBWUYfrJsd1dijBPTufrVE\"",
"snippet": {
"liveBroadcastContent": "none",
"description": "Make sure to apply adhesive evenly before use. GET IT HERE: http://adhesivewombat.bandcamp.com/album/marsupial-madness Facebook: ...",
"channelId": "UCps63j3krzAG4OyXeEyuhFw",
"title": "AdhesiveWombat - 8 Bit Adventure",
"channelTitle": "AdhesiveWombat",
"publishedAt": "2012-08-22T18:41:03.000Z",
"thumbnails": {
"medium": {
"url": "https://i.ytimg.com/vi/0HxZn6CzOIo/mqdefault.jpg",
"height": 180,
"width": 320
},
"high": {
"url": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg",
"height": 360,
"width": 480
},
"default": {
"url": "https://i.ytimg.com/vi/0HxZn6CzOIo/default.jpg",
"height": 90,
"width": 120
}
}
},
"kind": "youtube#searchResult"
},
{
"id": {
"videoId": "n4A_F5SXmgo",
"kind": "youtube#video"
},
"etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/aRVESw24jlgiErDgJKxNrazKRDc\"",
"snippet": {
"liveBroadcastContent": "none",
"description": "Free Download: http://bit.ly/1fZ1pMJ I don't post 8 bit'ish music much but damn I must admit this is goood! Enjoy \u2665 \u25bbSpikedGrin: ...",
"channelId": "UCMOgdURr7d8pOVlc-alkfRg",
"title": "\u3010Electro\u3011AdhesiveWombat - 8 Bit Adventure (SpikedGrin Remix) [Free Download]",
"channelTitle": "xKito Music",
"publishedAt": "2013-10-27T13:16:48.000Z",
"thumbnails": {
"medium": {
"url": "https://i.ytimg.com/vi/n4A_F5SXmgo/mqdefault.jpg",
"height": 180,
"width": 320
},
"high": {
"url": "https://i.ytimg.com/vi/n4A_F5SXmgo/hqdefault.jpg",
"height": 360,
"width": 480
},
"default": {
"url": "https://i.ytimg.com/vi/n4A_F5SXmgo/default.jpg",
"height": 90,
"width": 120
}
}
},
"kind": "youtube#searchResult"
},
],
"regionCode": "FR",
"kind": "youtube#searchListResponse"
}
search['system of a down toxicity'] = {
"items": [
{
"id": {
"kind": "youtube#video",
"videoId": "BorYwGi2SJc"
},
"kind": "youtube#searchResult",
"snippet": {
"title": "System of a Down: Toxicity",
"channelTitle": "Vedres Csaba",
"channelId": "UCBXeuQORNwPv4m68fgGMtPQ",
"thumbnails": {
"default": {
"height": 90,
"width": 120,
"url": "https://i.ytimg.com/vi/BorYwGi2SJc/default.jpg"
},
"high": {
"height": 360,
"width": 480,
"url": "https://i.ytimg.com/vi/BorYwGi2SJc/hqdefault.jpg"
},
"medium": {
"height": 180,
"width": 320,
"url": "https://i.ytimg.com/vi/BorYwGi2SJc/mqdefault.jpg"
}
},
"publishedAt": "2007-12-17T12:39:54.000Z",
"description": "http://www.vedrescsaba.uw.hu The System of a Down song Toxicity arranged for a classical piano quintet, played by Vedres Csaba and the Kairosz quartet.",
"liveBroadcastContent": "none"
},
"etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/UwR8H6P6kbijNZmBNkYd2jAzDnI\""
},
{
"id": {
"kind": "youtube#video",
"videoId": "ENBv2i88g6Y"
},
"kind": "youtube#searchResult",
"snippet": {
"title": "System Of A Down - Question!",
"channelTitle": "systemofadownVEVO",
"channelId": "UCvtZDkeFxMkRTNqfqXtxxkw",
"thumbnails": {
"default": {
"height": 90,
"width": 120,
"url": "https://i.ytimg.com/vi/ENBv2i88g6Y/default.jpg"
},
"high": {
"height": 360,
"width": 480,
"url": "https://i.ytimg.com/vi/ENBv2i88g6Y/hqdefault.jpg"
},
"medium": {
"height": 180,
"width": 320,
"url": "https://i.ytimg.com/vi/ENBv2i88g6Y/mqdefault.jpg"
}
},
"publishedAt": "2009-10-03T04:49:03.000Z",
"description": "System of a Down's official music video for 'Question!'. Click to listen to System of a Down on Spotify: http://smarturl.it/SystemSpotify?IQid=SystemQu As featured ...",
"liveBroadcastContent": "none"
},
"etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/dB-M0N9mB4xE-k4yAF_4d8aU0I4\""
},
],
"etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/yhLQgSpeObNnybd5JqSzlGiJ8Ew\"",
"nextPageToken": "CBkQAA",
"pageInfo": {
"resultsPerPage": 25,
"totalResults": 26825
},
"kind": "youtube#searchListResponse",
"regionCode": "FR"
}

View file

@ -0,0 +1,74 @@
import json
from collections import OrderedDict
import unittest
from test_plus.test import TestCase
from django.core.urlresolvers import reverse
from funkwhale_api.providers.youtube.client import client
from . import data as api_data
class TestAPI(TestCase):
@unittest.mock.patch(
'funkwhale_api.providers.youtube.client._do_search',
return_value=api_data.search['8 bit adventure'])
def test_can_get_search_results_from_youtube(self, *mocks):
query = '8 bit adventure'
results = client.search(query)
self.assertEqual(results[0]['id']['videoId'], '0HxZn6CzOIo')
self.assertEqual(results[0]['snippet']['title'], 'AdhesiveWombat - 8 Bit Adventure')
self.assertEqual(results[0]['full_url'], 'https://www.youtube.com/watch?v=0HxZn6CzOIo')
@unittest.mock.patch(
'funkwhale_api.providers.youtube.client._do_search',
return_value=api_data.search['8 bit adventure'])
def test_can_get_search_results_from_funkwhale(self, *mocks):
query = '8 bit adventure'
expected = json.dumps(client.search(query))
url = self.reverse('api:providers:youtube:search')
response = self.client.get(url + '?query={0}'.format(query))
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
@unittest.mock.patch(
'funkwhale_api.providers.youtube.client._do_search',
side_effect=[
api_data.search['8 bit adventure'],
api_data.search['system of a down toxicity'],
]
)
def test_can_send_multiple_queries_at_once(self, *mocks):
queries = OrderedDict()
queries['1'] = {
'q': '8 bit adventure',
}
queries['2'] = {
'q': 'system of a down toxicity',
}
results = client.search_multiple(queries)
self.assertEqual(results['1'][0]['id']['videoId'], '0HxZn6CzOIo')
self.assertEqual(results['1'][0]['snippet']['title'], 'AdhesiveWombat - 8 Bit Adventure')
self.assertEqual(results['1'][0]['full_url'], 'https://www.youtube.com/watch?v=0HxZn6CzOIo')
self.assertEqual(results['2'][0]['id']['videoId'], 'BorYwGi2SJc')
self.assertEqual(results['2'][0]['snippet']['title'], 'System of a Down: Toxicity')
self.assertEqual(results['2'][0]['full_url'], 'https://www.youtube.com/watch?v=BorYwGi2SJc')
@unittest.mock.patch(
'funkwhale_api.providers.youtube.client._do_search',
return_value=api_data.search['8 bit adventure'],
)
def test_can_send_multiple_queries_at_once_from_funwkhale(self, *mocks):
queries = OrderedDict()
queries['1'] = {
'q': '8 bit adventure',
}
expected = json.dumps(client.search_multiple(queries))
url = self.reverse('api:providers:youtube:searchs')
response = self.client.post(
url, json.dumps(queries), content_type='application/json')
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))

View file

@ -0,0 +1,8 @@
from django.conf.urls import include, url
from .views import APISearch, APISearchs
urlpatterns = [
url(r'^search/$', APISearch.as_view(), name='search'),
url(r'^searchs/$', APISearchs.as_view(), name='searchs'),
]

View file

@ -0,0 +1,21 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from funkwhale_api.common.permissions import ConditionalAuthentication
from .client import client
class APISearch(APIView):
permission_classes = [ConditionalAuthentication]
def get(self, request, *args, **kwargs):
results = client.search(request.GET['query'])
return Response(results)
class APISearchs(APIView):
permission_classes = [ConditionalAuthentication]
def post(self, request, *args, **kwargs):
results = client.search_multiple(request.data)
return Response(results)