Commit 8335b1ac authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'release/0.1'

parents e18754db 73ee65af
Pipeline #100 passed with stage
in 26 seconds
BACKEND_URL=http://localhost:6001
BACKEND_URL=http://localhost:12081
YOUTUBE_API_KEY=
API_AUTHENTICATION_REQUIRED=False
variables:
IMAGE_NAME: funkwhale/funkwhale
IMAGE: $IMAGE_NAME:$CI_BUILD_REF
IMAGE_LATEST: $IMAGE_NAME:latest
stages:
- test
- build
- deploy
test_api:
stage: test
image: funkwhale/funkwhale:base
before_script:
- docker-compose -f api/test.yml build
- cd api
- pip install -r requirements/test.txt
script:
- docker-compose -f api/test.yml run test
after_script:
- docker-compose -f api/test.yml run test rm -rf funkwhale_api/media/
- pytest
tags:
- dind
- docker
build_front:
stage: build
......@@ -53,6 +61,33 @@ pages:
paths:
- public
only:
- master
- develop
tags:
- docker
docker_develop:
stage: deploy
before_script:
- docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
- cd api
script:
- docker build -t $IMAGE .
- docker push $IMAGE
only:
- develop
tags:
- dind
docker_release:
stage: deploy
before_script:
- docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
- cd api
script:
- docker build -t $IMAGE -t $IMAGE_LATEST .
- docker push $IMAGE
- docker push $IMAGE_LATEST
only:
- master
tags:
- dind
......@@ -2,6 +2,8 @@ from rest_framework import routers
from django.conf.urls import include, url
from funkwhale_api.music import views
from funkwhale_api.playlists import views as playlists_views
from rest_framework_jwt import views as jwt_views
router = routers.SimpleRouter()
router.register(r'tags', views.TagViewSet, 'tags')
......@@ -12,15 +14,19 @@ router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches')
router.register(r'submit', views.SubmitViewSet, 'submit')
router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists')
router.register(r'playlist-tracks', playlists_views.PlaylistTrackViewSet, 'playlist-tracks')
urlpatterns = router.urls
v1_patterns = router.urls
urlpatterns += [
v1_patterns += [
url(r'^providers/', include('funkwhale_api.providers.urls', namespace='providers')),
url(r'^favorites/', include('funkwhale_api.favorites.urls', namespace='favorites')),
url(r'^search$', views.Search.as_view(), name='search'),
url(r'^radios/', include('funkwhale_api.radios.urls', namespace='radios')),
url(r'^history/', include('funkwhale_api.history.urls', namespace='history')),
url(r'^users/', include('funkwhale_api.users.api_urls', namespace='users')),
url(r'^token/', 'rest_framework_jwt.views.obtain_jwt_token'),
url(r'^token/refresh/', 'rest_framework_jwt.views.refresh_jwt_token'),
url(r'^token/', jwt_views.obtain_jwt_token),
url(r'^token/refresh/', jwt_views.refresh_jwt_token),
]
urlpatterns = [
url(r'^v1/', include(v1_patterns, namespace='v1'))
]
......@@ -199,7 +199,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap3'
STATIC_ROOT = str(ROOT_DIR('staticfiles'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = env("STATIC_URL", default='/static/')
STATIC_URL = env("STATIC_URL", default='/staticfiles/')
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = (
......
......@@ -24,7 +24,7 @@ class TestFavorites(TestCase):
def test_user_can_get_his_favorites(self):
favorite = TrackFavorite.add(self.track, self.user)
url = reverse('api:favorites:tracks-list')
url = reverse('api:v1:favorites:tracks-list')
self.client.login(username=self.user.username, password='test')
response = self.client.get(url)
......@@ -41,7 +41,7 @@ class TestFavorites(TestCase):
self.assertEqual(expected, parsed_json['results'])
def test_user_can_add_favorite_via_api(self):
url = reverse('api:favorites:tracks-list')
url = reverse('api:v1:favorites:tracks-list')
self.client.login(username=self.user.username, password='test')
response = self.client.post(url, {'track': self.track.pk})
......@@ -60,7 +60,7 @@ class TestFavorites(TestCase):
def test_user_can_remove_favorite_via_api(self):
favorite = TrackFavorite.add(self.track, self.user)
url = reverse('api:favorites:tracks-detail', kwargs={'pk': favorite.pk})
url = reverse('api:v1:favorites:tracks-detail', kwargs={'pk': favorite.pk})
self.client.login(username=self.user.username, password='test')
response = self.client.delete(url, {'track': self.track.pk})
self.assertEqual(response.status_code, 204)
......@@ -69,7 +69,7 @@ class TestFavorites(TestCase):
def test_user_can_remove_favorite_via_api_using_track_id(self):
favorite = TrackFavorite.add(self.track, self.user)
url = reverse('api:favorites:tracks-remove')
url = reverse('api:v1:favorites:tracks-remove')
self.client.login(username=self.user.username, password='test')
response = self.client.delete(
url, json.dumps({'track': self.track.pk}),
......@@ -83,7 +83,7 @@ class TestFavorites(TestCase):
def test_can_restrict_api_views_to_authenticated_users(self):
urls = [
('api:favorites:tracks-list', 'get'),
('api:v1:favorites:tracks-list', 'get'),
]
for route_name, method in urls:
......@@ -103,7 +103,7 @@ class TestFavorites(TestCase):
def test_can_filter_tracks_by_favorites(self):
favorite = TrackFavorite.add(self.track, self.user)
url = reverse('api:tracks-list')
url = reverse('api:v1:tracks-list')
self.client.login(username=self.user.username, password='test')
response = self.client.get(url, data={'favorites': True})
......
......@@ -23,7 +23,7 @@ class TestHistory(TestCase):
def test_anonymous_user_can_create_listening_via_api(self):
track = mommy.make('music.Track')
url = self.reverse('api:history:listenings-list')
url = self.reverse('api:v1:history:listenings-list')
response = self.client.post(url, {
'track': track.pk,
})
......@@ -38,7 +38,7 @@ class TestHistory(TestCase):
self.client.login(username=self.user.username, password='test')
url = self.reverse('api:history:listenings-list')
url = self.reverse('api:v1:history:listenings-list')
response = self.client.post(url, {
'track': track.pk,
})
......
import mutagen
import arrow
NODEFAULT = object()
class Metadata(object):
ALIASES = {
'release': 'musicbrainz_albumid',
'artist': 'musicbrainz_artistid',
'recording': 'musicbrainz_trackid',
class TagNotFound(KeyError):
pass
def get_id3_tag(f, k):
# First we try to grab the standard key
try:
return f.tags[k].text[0]
except KeyError:
pass
# then we fallback on parsing non standard tags
all_tags = f.tags.getall('TXXX')
try:
matches = [
t
for t in all_tags
if t.desc.lower() == k.lower()
]
return matches[0].text[0]
except (KeyError, IndexError):
raise TagNotFound(k)
def get_mp3_recording_id(f, k):
try:
return [
t
for t in f.tags.getall('UFID')
if 'musicbrainz.org' in t.owner
][0].data.decode('utf-8')
except IndexError:
raise TagNotFound(k)
CONF = {
'OggVorbis': {
'getter': lambda f, k: f[k][0],
'fields': {
'track_number': {
'field': 'TRACKNUMBER',
'to_application': int
},
'title': {
'field': 'title'
},
'artist': {
'field': 'artist'
},
'album': {
'field': 'album'
},
'date': {
'field': 'date',
'to_application': lambda v: arrow.get(v).date()
},
'musicbrainz_albumid': {
'field': 'musicbrainz_albumid'
},
'musicbrainz_artistid': {
'field': 'musicbrainz_artistid'
},
'musicbrainz_recordingid': {
'field': 'musicbrainz_trackid'
},
}
},
'MP3': {
'getter': get_id3_tag,
'fields': {
'track_number': {
'field': 'TPOS',
'to_application': lambda v: int(v.split('/')[0])
},
'title': {
'field': 'TIT2'
},
'artist': {
'field': 'TPE1'
},
'album': {
'field': 'TALB'
},
'date': {
'field': 'TDRC',
'to_application': lambda v: arrow.get(str(v)).date()
},
'musicbrainz_albumid': {
'field': 'MusicBrainz Album Id'
},
'musicbrainz_artistid': {
'field': 'MusicBrainz Artist Id'
},
'musicbrainz_recordingid': {
'field': 'UFID',
'getter': get_mp3_recording_id,
},
}
}
}
class Metadata(object):
def __init__(self, path):
self._file = mutagen.File(path)
self._conf = CONF[self.get_file_type(self._file)]
def get(self, key, default=NODEFAULT, single=True):
def get_file_type(self, f):
return f.__class__.__name__
def get(self, key, default=NODEFAULT):
field_conf = self._conf['fields'][key]
real_key = field_conf['field']
try:
v = self._file[key]
getter = field_conf.get('getter', self._conf['getter'])
v = getter(self._file, real_key)
except KeyError:
if default == NODEFAULT:
raise
raise TagNotFound(real_key)
return default
# Some tags are returned as lists of string
if single:
return v[0]
converter = field_conf.get('to_application')
if converter:
v = converter(v)
return v
def __getattr__(self, key):
try:
alias = self.ALIASES[key]
except KeyError:
raise ValueError('Invalid alias {}'.format(key))
return self.get(alias, single=True)
......@@ -314,7 +314,7 @@ class Track(APIModelMixin):
return work
def get_lyrics_url(self):
return reverse('api:tracks-lyrics', kwargs={'pk': self.pk})
return reverse('api:v1:tracks-lyrics', kwargs={'pk': self.pk})
@property
def full_name(self):
......
......@@ -20,7 +20,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
def test_can_submit_youtube_url_for_track_import(self, *mocks):
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
video_id = 'tPEE9ZwTmy0'
url = reverse('api:submit-single')
url = reverse('api:v1:submit-single')
user = User.objects.create_superuser(username='test', email='test@test.com', password='test')
self.client.login(username=user.username, password='test')
response = self.client.post(url, {'import_url': 'https://www.youtube.com/watch?v={0}'.format(video_id), 'mbid': mbid})
......@@ -33,7 +33,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
user = User.objects.create_superuser(username='test', email='test@test.com', password='test')
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
video_id = 'tPEE9ZwTmy0'
url = reverse('api:submit-single')
url = reverse('api:v1:submit-single')
self.client.login(username=user.username, password='test')
with self.settings(CELERY_ALWAYS_EAGER=False):
response = self.client.post(url, {'import_url': 'https://www.youtube.com/watch?v={0}'.format(video_id), 'mbid': mbid})
......@@ -69,7 +69,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
},
]
}
url = reverse('api:submit-album')
url = reverse('api:v1:submit-album')
self.client.login(username=user.username, password='test')
with self.settings(CELERY_ALWAYS_EAGER=False):
response = self.client.post(url, json.dumps(payload), content_type="application/json")
......@@ -123,7 +123,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
}
]
}
url = reverse('api:submit-artist')
url = reverse('api:v1:submit-artist')
self.client.login(username=user.username, password='test')
with self.settings(CELERY_ALWAYS_EAGER=False):
response = self.client.post(url, json.dumps(payload), content_type="application/json")
......@@ -159,7 +159,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
batch = models.ImportBatch.objects.create(submitted_by=user1)
job = models.ImportJob.objects.create(batch=batch, mbid=mbid, source=source)
url = reverse('api:import-batches-list')
url = reverse('api:v1:import-batches-list')
self.client.login(username=user2.username, password='test')
response2 = self.client.get(url)
......@@ -175,7 +175,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
artist2 = models.Artist.objects.create(name='Test2')
query = 'test1'
expected = '[{0}]'.format(json.dumps(serializers.ArtistSerializerNested(artist1).data))
url = self.reverse('api:artists-search')
url = self.reverse('api:v1:artists-search')
response = self.client.get(url + '?query={0}'.format(query))
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
......@@ -187,17 +187,17 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
track2 = models.Track.objects.create(artist=artist2, title="test_track2")
query = 'test track 1'
expected = '[{0}]'.format(json.dumps(serializers.TrackSerializerNested(track1).data))
url = self.reverse('api:tracks-search')
url = self.reverse('api:v1:tracks-search')
response = self.client.get(url + '?query={0}'.format(query))
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
def test_can_restrict_api_views_to_authenticated_users(self):
urls = [
('api:tags-list', 'get'),
('api:tracks-list', 'get'),
('api:artists-list', 'get'),
('api:albums-list', 'get'),
('api:v1:tags-list', 'get'),
('api:v1:tracks-list', 'get'),
('api:v1:artists-list', 'get'),
('api:v1:albums-list', 'get'),
]
for route_name, method in urls:
......
......@@ -59,7 +59,7 @@ Is it me you're looking for?"""
work=None,
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
url = reverse('api:tracks-lyrics', kwargs={'pk': track.pk})
url = reverse('api:v1:tracks-lyrics', kwargs={'pk': track.pk})
user = User.objects.create_user(
username='test', email='test@test.com', password='test')
self.client.login(username=user.username, password='test')
......
import unittest
import os
import datetime
from test_plus.test import TestCase
from funkwhale_api.music import metadata
......@@ -8,20 +9,72 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__))
class TestMetadata(TestCase):
def test_can_get_metadata_from_file(self, *mocks):
def test_can_get_metadata_from_ogg_file(self, *mocks):
path = os.path.join(DATA_DIR, 'test.ogg')
data = metadata.Metadata(path)
self.assertEqual(
data.get('title'),
'Peer Gynt Suite no. 1, op. 46: I. Morning'
)
self.assertEqual(
data.get('artist'),
'Edvard Grieg'
)
self.assertEqual(
data.get('album'),
'Peer Gynt Suite no. 1, op. 46'
)
self.assertEqual(
data.get('date'),
datetime.date(2012, 8, 15),
)
self.assertEqual(
data.get('track_number'),
1
)
self.assertEqual(
data.get('musicbrainz_albumid'),
'a766da8b-8336-47aa-a3ee-371cc41ccc75')
self.assertEqual(
data.get('musicbrainz_trackid'),
data.get('musicbrainz_recordingid'),
'bd21ac48-46d8-4e78-925f-d9cc2a294656')
self.assertEqual(
data.get('musicbrainz_artistid'),
'013c8e5b-d72a-4cd3-8dee-6c64d6125823')
self.assertEqual(data.release, data.get('musicbrainz_albumid'))
self.assertEqual(data.artist, data.get('musicbrainz_artistid'))
self.assertEqual(data.recording, data.get('musicbrainz_trackid'))
def test_can_get_metadata_from_id3_mp3_file(self, *mocks):
path = os.path.join(DATA_DIR, 'test.mp3')
data = metadata.Metadata(path)
self.assertEqual(
data.get('title'),
'Bend'
)
self.assertEqual(
data.get('artist'),
'Binärpilot'
)
self.assertEqual(
data.get('album'),
'You Can\'t Stop Da Funk'
)
self.assertEqual(
data.get('date'),
datetime.date(2006, 2, 7),
)
self.assertEqual(
data.get('track_number'),
1
)
self.assertEqual(
data.get('musicbrainz_albumid'),
'ce40cdb1-a562-4fd8-a269-9269f98d4124')
self.assertEqual(
data.get('musicbrainz_recordingid'),
'f269d497-1cc0-4ae4-a0c4-157ec7d73fcb')
self.assertEqual(
data.get('musicbrainz_artistid'),
'9c6bddde-6228-4d9f-ad0d-03f6fcb19e13')
......@@ -121,13 +121,14 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
class TagViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Tag.objects.all()
queryset = Tag.objects.all().order_by('name')
serializer_class = serializers.TagSerializer
permission_classes = [ConditionalAuthentication]
class Search(views.APIView):
max_results = 3
def get(self, request, *args, **kwargs):
query = request.GET['query']
results = {
......
......@@ -13,7 +13,7 @@ class TestAPI(TestCase):
return_value=api_data.recordings['search']['brontide matador'])
def test_can_search_recording_in_musicbrainz_api(self, *mocks):
query = 'brontide matador'
url = reverse('api:providers:musicbrainz:search-recordings')
url = reverse('api:v1:providers:musicbrainz:search-recordings')
expected = api_data.recordings['search']['brontide matador']
response = self.client.get(url, data={'query': query})
......@@ -24,7 +24,7 @@ class TestAPI(TestCase):
return_value=api_data.releases['search']['brontide matador'])
def test_can_search_release_in_musicbrainz_api(self, *mocks):
query = 'brontide matador'
url = reverse('api:providers:musicbrainz:search-releases')
url = reverse('api:v1:providers:musicbrainz:search-releases')
expected = api_data.releases['search']['brontide matador']
response = self.client.get(url, data={'query': query})
......@@ -35,7 +35,7 @@ class TestAPI(TestCase):
return_value=api_data.artists['search']['lost fingers'])
def test_can_search_artists_in_musicbrainz_api(self, *mocks):
query = 'lost fingers'
url = reverse('api:providers:musicbrainz:search-artists')
url = reverse('api:v1:providers:musicbrainz:search-artists')
expected = api_data.artists['search']['lost fingers']
response = self.client.get(url, data={'query': query})
......@@ -46,7 +46,7 @@ class TestAPI(TestCase):
return_value=api_data.artists['get']['lost fingers'])
def test_can_get_artist_in_musicbrainz_api(self, *mocks):
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
url = reverse('api:providers:musicbrainz:artist-detail', kwargs={
url = reverse('api:v1:providers:musicbrainz:artist-detail', kwargs={
'uuid': uuid,
})
response = self.client.get(url)
......@@ -60,7 +60,7 @@ class TestAPI(TestCase):
def test_can_broswe_release_group_using_musicbrainz_api(self, *mocks):
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
url = reverse(
'api:providers:musicbrainz:release-group-browse',
'api:v1:providers:musicbrainz:release-group-browse',
kwargs={
'artist_uuid': uuid,
}
......@@ -76,7 +76,7 @@ class TestAPI(TestCase):
def test_can_broswe_releases_using_musicbrainz_api(self, *mocks):
uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1'
url = reverse(
'api:providers:musicbrainz:release-browse',
'api:v1:providers:musicbrainz:release-browse',
kwargs={
'release_group_uuid': uuid,
}
......
......@@ -38,7 +38,7 @@ class TestPlayLists(TestCase):
def test_can_create_playlist_via_api(self):
self.client.login(username=self.user.username, password='test')
url = reverse('api:playlists-list')
url = reverse('api:v1:playlists-list')
data = {
'name': 'test',
}
......@@ -54,7 +54,7 @@ class TestPlayLists(TestCase):
self.client.login(username=self.