diff --git a/.env.dev b/.env.dev index 75954b6bc..de58e2758 100644 --- a/.env.dev +++ b/.env.dev @@ -1,3 +1,3 @@ -BACKEND_URL=http://localhost:6001 +BACKEND_URL=http://localhost:12081 YOUTUBE_API_KEY= API_AUTHENTICATION_REQUIRED=False diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4a10a92e0..fd73e4bc0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,18 +1,26 @@ +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 diff --git a/api/config/api_urls.py b/api/config/api_urls.py index 4ed81d284..b56944d4e 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -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')) ] diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 5c55dfdc1..93381c4f5 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -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 = ( diff --git a/api/funkwhale_api/favorites/tests/test_favorites.py b/api/funkwhale_api/favorites/tests/test_favorites.py index a68659ba4..230f030fe 100644 --- a/api/funkwhale_api/favorites/tests/test_favorites.py +++ b/api/funkwhale_api/favorites/tests/test_favorites.py @@ -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}) diff --git a/api/funkwhale_api/history/tests/test_history.py b/api/funkwhale_api/history/tests/test_history.py index 448c5b15e..61009615a 100644 --- a/api/funkwhale_api/history/tests/test_history.py +++ b/api/funkwhale_api/history/tests/test_history.py @@ -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, }) diff --git a/api/funkwhale_api/music/metadata.py b/api/funkwhale_api/music/metadata.py index 3fe61e652..d1be9a4e1 100644 --- a/api/funkwhale_api/music/metadata.py +++ b/api/funkwhale_api/music/metadata.py @@ -1,34 +1,130 @@ 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) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 8d4e84a23..702308477 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -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): diff --git a/api/funkwhale_api/music/tests/test.mp3 b/api/funkwhale_api/music/tests/test.mp3 new file mode 100644 index 000000000..35a6e5fce Binary files /dev/null and b/api/funkwhale_api/music/tests/test.mp3 differ diff --git a/api/funkwhale_api/music/tests/test_api.py b/api/funkwhale_api/music/tests/test_api.py index ff3deedf3..d8f56eeb9 100644 --- a/api/funkwhale_api/music/tests/test_api.py +++ b/api/funkwhale_api/music/tests/test_api.py @@ -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: diff --git a/api/funkwhale_api/music/tests/test_lyrics.py b/api/funkwhale_api/music/tests/test_lyrics.py index f74ecb2ef..0ea22371b 100644 --- a/api/funkwhale_api/music/tests/test_lyrics.py +++ b/api/funkwhale_api/music/tests/test_lyrics.py @@ -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') diff --git a/api/funkwhale_api/music/tests/test_metadata.py b/api/funkwhale_api/music/tests/test_metadata.py index 7832baedb..9b8c76653 100644 --- a/api/funkwhale_api/music/tests/test_metadata.py +++ b/api/funkwhale_api/music/tests/test_metadata.py @@ -1,5 +1,6 @@ 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') diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 4887ad139..772f4173e 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -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 = { diff --git a/api/funkwhale_api/musicbrainz/tests/test_api.py b/api/funkwhale_api/musicbrainz/tests/test_api.py index 1734c943c..f962e0f78 100644 --- a/api/funkwhale_api/musicbrainz/tests/test_api.py +++ b/api/funkwhale_api/musicbrainz/tests/test_api.py @@ -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, } diff --git a/api/funkwhale_api/playlists/tests/test_playlists.py b/api/funkwhale_api/playlists/tests/test_playlists.py index 056ca06e6..49025cd11 100644 --- a/api/funkwhale_api/playlists/tests/test_playlists.py +++ b/api/funkwhale_api/playlists/tests/test_playlists.py @@ -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.user.username, password='test') - url = reverse('api:playlist-tracks-list') + url = reverse('api:v1:playlist-tracks-list') data = { 'playlist': playlist.pk, 'track': tracks[0].pk diff --git a/api/funkwhale_api/providers/audiofile/importer.py b/api/funkwhale_api/providers/audiofile/importer.py index d95c120e1..9e1b0fb3f 100644 --- a/api/funkwhale_api/providers/audiofile/importer.py +++ b/api/funkwhale_api/providers/audiofile/importer.py @@ -9,41 +9,34 @@ 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')}, + defaults={ + 'name': data.get('artist'), + 'mbid': data.get('musicbrainz_artistid', None), + + }, )[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 - + release_date = data.get('date', default=None) album = models.Album.objects.get_or_create( title__iexact=data.get('album'), artist=artist, defaults={ 'title': data.get('album'), 'release_date': release_date, + 'mbid': data.get('musicbrainz_albumid', None), }, )[0] - position = None - try: - position = int(data.get('tracknumber', None)) - except ValueError: - pass + position = data.get('track_number', default=None) track = models.Track.objects.get_or_create( title__iexact=data.get('title'), album=album, defaults={ 'title': data.get('title'), 'position': position, + 'mbid': data.get('musicbrainz_recordingid', None), }, )[0] diff --git a/api/funkwhale_api/providers/audiofile/tests/test_disk_import.py b/api/funkwhale_api/providers/audiofile/tests/test_disk_import.py index 26532a8c9..4a91a36eb 100644 --- a/api/funkwhale_api/providers/audiofile/tests/test_disk_import.py +++ b/api/funkwhale_api/providers/audiofile/tests/test_disk_import.py @@ -14,21 +14,37 @@ class TestAudioFile(TestCase): 'artist': ['Test artist'], 'album': ['Test album'], 'title': ['Test track'], - 'tracknumber': ['4'], - 'date': ['2012-08-15'] + 'TRACKNUMBER': ['4'], + 'date': ['2012-08-15'], + 'musicbrainz_albumid': ['a766da8b-8336-47aa-a3ee-371cc41ccc75'], + 'musicbrainz_trackid': ['bd21ac48-46d8-4e78-925f-d9cc2a294656'], + 'musicbrainz_artistid': ['013c8e5b-d72a-4cd3-8dee-6c64d6125823'], } - with unittest.mock.patch('mutagen.File', return_value=metadata): + m1 = unittest.mock.patch('mutagen.File', return_value=metadata) + m2 = unittest.mock.patch( + 'funkwhale_api.music.metadata.Metadata.get_file_type', + return_value='OggVorbis', + ) + with m1, m2: 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.mbid, metadata['musicbrainz_trackid'][0]) self.assertEqual( track_file.track.position, 4) self.assertEqual( track_file.track.album.title, metadata['album'][0]) + self.assertEqual( + track_file.track.album.mbid, + metadata['musicbrainz_albumid'][0]) self.assertEqual( track_file.track.album.release_date, datetime.date(2012, 8, 15)) self.assertEqual( track_file.track.artist.name, metadata['artist'][0]) + self.assertEqual( + track_file.track.artist.mbid, + metadata['musicbrainz_artistid'][0]) diff --git a/api/funkwhale_api/providers/youtube/tests/test_youtube.py b/api/funkwhale_api/providers/youtube/tests/test_youtube.py index 56b87a354..ca0a95628 100644 --- a/api/funkwhale_api/providers/youtube/tests/test_youtube.py +++ b/api/funkwhale_api/providers/youtube/tests/test_youtube.py @@ -26,7 +26,7 @@ class TestAPI(TestCase): 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') + url = self.reverse('api:v1:providers:youtube:search') response = self.client.get(url + '?query={0}'.format(query)) self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8'))) @@ -67,7 +67,7 @@ class TestAPI(TestCase): } expected = json.dumps(client.search_multiple(queries)) - url = self.reverse('api:providers:youtube:searchs') + url = self.reverse('api:v1:providers:youtube:searchs') response = self.client.post( url, json.dumps(queries), content_type='application/json') diff --git a/api/funkwhale_api/radios/tests/test_radios.py b/api/funkwhale_api/radios/tests/test_radios.py index 7d069be9c..5729b4412 100644 --- a/api/funkwhale_api/radios/tests/test_radios.py +++ b/api/funkwhale_api/radios/tests/test_radios.py @@ -94,7 +94,7 @@ class TestRadios(TestCase): self.assertEqual(radio.session, restarted_radio.session) def test_can_get_start_radio_from_api(self): - url = reverse('api:radios:sessions-list') + url = reverse('api:v1:radios:sessions-list') response = self.client.post(url, {'radio_type': 'random'}) session = models.RadioSession.objects.latest('id') self.assertEqual(session.radio_type, 'random') @@ -107,7 +107,7 @@ class TestRadios(TestCase): self.assertEqual(session.user, self.user) def test_can_start_radio_for_anonymous_user(self): - url = reverse('api:radios:sessions-list') + url = reverse('api:v1:radios:sessions-list') response = self.client.post(url, {'radio_type': 'random'}) session = models.RadioSession.objects.latest('id') @@ -118,11 +118,11 @@ class TestRadios(TestCase): tracks = mommy.make('music.Track', _quantity=1) self.client.login(username=self.user.username, password='test') - url = reverse('api:radios:sessions-list') + url = reverse('api:v1:radios:sessions-list') response = self.client.post(url, {'radio_type': 'random'}) session = models.RadioSession.objects.latest('id') - url = reverse('api:radios:tracks-list') + url = reverse('api:v1:radios:tracks-list') response = self.client.post(url, {'session': session.pk}) data = json.loads(response.content.decode('utf-8')) @@ -173,7 +173,7 @@ class TestRadios(TestCase): def test_can_start_artist_radio_from_api(self): artist = mommy.make('music.Artist') - url = reverse('api:radios:sessions-list') + url = reverse('api:v1:radios:sessions-list') response = self.client.post(url, {'radio_type': 'artist', 'related_object_id': artist.id}) session = models.RadioSession.objects.latest('id') diff --git a/api/funkwhale_api/templates/account/base.html b/api/funkwhale_api/templates/account/base.html deleted file mode 100644 index c64b47a4a..000000000 --- a/api/funkwhale_api/templates/account/base.html +++ /dev/null @@ -1,2 +0,0 @@ -{% extends "base.html" %} -{% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %} diff --git a/api/funkwhale_api/templates/account/email.html b/api/funkwhale_api/templates/account/email.html deleted file mode 100644 index 7c52a4a27..000000000 --- a/api/funkwhale_api/templates/account/email.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load crispy_forms_tags %} - -{% block head_title %}{% trans "Account" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "E-mail Addresses" %}

- {% if user.emailaddress_set.all %} -

{% trans 'The following e-mail addresses are associated with your account:' %}

- - - - {% else %} -

{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

- - {% endif %} - - -

{% trans "Add E-mail Address" %}

- -
- {% csrf_token %} - {{ form|crispy }} - -
-
-
-
-{% endblock %} - - -{% block extra_body %} - -{% endblock %} - diff --git a/api/funkwhale_api/templates/account/email_confirm.html b/api/funkwhale_api/templates/account/email_confirm.html deleted file mode 100644 index 4395d46bf..000000000 --- a/api/funkwhale_api/templates/account/email_confirm.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load account %} - -{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} - - -{% block content %} -
-
-
-

{% trans "Confirm E-mail Address" %}

- - {% if confirmation %} - - {% user_display confirmation.email_address.user as user_display %} - -

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

- -
- {% csrf_token %} - -
- - {% else %} - - {% url 'account_email' as email_url %} - -

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

- - {% endif %} -
-
-
-{% endblock %} - diff --git a/api/funkwhale_api/templates/account/email_confirmed.html b/api/funkwhale_api/templates/account/email_confirmed.html deleted file mode 100644 index 7b2edf08b..000000000 --- a/api/funkwhale_api/templates/account/email_confirmed.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load account %} - -{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} - - -{% block content %} -
-
-
-

{% trans "Confirm E-mail Address" %}

- - {% user_display email_address.user as user_display %} - -

{% blocktrans with email_address.email as email %}You have confirmed that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

-
-
-
-{% endblock %} diff --git a/api/funkwhale_api/templates/account/login.html b/api/funkwhale_api/templates/account/login.html deleted file mode 100644 index 4a05dc772..000000000 --- a/api/funkwhale_api/templates/account/login.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load account %} -{% load socialaccount %} -{% load crispy_forms_tags %} - -{% block head_title %}{% trans "Sign In" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Sign In" %}

- {% get_providers as socialaccount_providers %} - {% if socialaccount_providers %} -

{% blocktrans with site.name as site_name %}Please sign in with one - of your existing third party accounts. Or, sign up - for a {{ site_name }} account and sign in below:{% endblocktrans %}

- -
- -
    - {% include "socialaccount/snippets/provider_list.html" with process="login" %} -
- - - -
- - {% include "socialaccount/snippets/login_extra.html" %} - - {% endif %} - - -
-
-
-{% endblock %} - diff --git a/api/funkwhale_api/templates/account/logout.html b/api/funkwhale_api/templates/account/logout.html deleted file mode 100644 index 4f1f8f3e6..000000000 --- a/api/funkwhale_api/templates/account/logout.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} - -{% block head_title %}{% trans "Sign Out" %}{% endblock %} - -{% block content %} -
-
-
- -

{% trans "Sign Out" %}

- -

{% trans 'Are you sure you want to sign out?' %}

- -
- {% csrf_token %} - {% if redirect_field_value %} - - {% endif %} - -
-
-
-
- -{% endblock %} - diff --git a/api/funkwhale_api/templates/account/password_change.html b/api/funkwhale_api/templates/account/password_change.html deleted file mode 100644 index e282b14ad..000000000 --- a/api/funkwhale_api/templates/account/password_change.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load crispy_forms_tags %} -{% block head_title %}{% trans "Change Password" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Change Password" %}

- -
- {% csrf_token %} - {{ form|crispy }} - -
-
-
-
-{% endblock %} - diff --git a/api/funkwhale_api/templates/account/password_reset.html b/api/funkwhale_api/templates/account/password_reset.html deleted file mode 100644 index efdfbd05c..000000000 --- a/api/funkwhale_api/templates/account/password_reset.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load account %} -{% load crispy_forms_tags %} - -{% block head_title %}{% trans "Password Reset" %}{% endblock %} - -{% block content %} -
-
-
- -

{% trans "Password Reset" %}

- {% if user.is_authenticated %} - {% include "account/snippets/already_logged_in.html" %} - {% endif %} - -

{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

- -
- {% csrf_token %} - {{ form|crispy }} - -
- -

{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}

-
-
-
-{% endblock %} - -{% block javascript %} - {{ block.super }} - -{% endblock javascript %} - diff --git a/api/funkwhale_api/templates/account/password_reset_done.html b/api/funkwhale_api/templates/account/password_reset_done.html deleted file mode 100644 index 865ecaa0a..000000000 --- a/api/funkwhale_api/templates/account/password_reset_done.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load account %} - -{% block head_title %}{% trans "Password Reset" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Password Reset" %}

- - {% if user.is_authenticated %} - {% include "account/snippets/already_logged_in.html" %} - {% endif %} - -

{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

-
-
-
-{% endblock %} diff --git a/api/funkwhale_api/templates/account/password_reset_from_key.html b/api/funkwhale_api/templates/account/password_reset_from_key.html deleted file mode 100644 index 41b50843f..000000000 --- a/api/funkwhale_api/templates/account/password_reset_from_key.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load crispy_forms_tags %} - -{% block head_title %}{% trans "Change Password" %}{% endblock %} - -{% block content %} -
-
-
-

{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}

- - {% if token_fail %} - {% url 'account_reset_password' as passwd_reset_url %} -

{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}

- {% else %} - {% if form %} -
- {% csrf_token %} - {{ form|crispy }} - -
- {% else %} -

{% trans 'Your password is now changed.' %}

- {% endif %} - {% endif %} -
-
-
-{% endblock %} - diff --git a/api/funkwhale_api/templates/account/password_reset_from_key_done.html b/api/funkwhale_api/templates/account/password_reset_from_key_done.html deleted file mode 100644 index e940eb83d..000000000 --- a/api/funkwhale_api/templates/account/password_reset_from_key_done.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% block head_title %}{% trans "Change Password" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Change Password" %}

-

{% trans 'Your password is now changed.' %}

-
-
-
-{% endblock %} - diff --git a/api/funkwhale_api/templates/account/password_set.html b/api/funkwhale_api/templates/account/password_set.html deleted file mode 100644 index 03e2fcd2d..000000000 --- a/api/funkwhale_api/templates/account/password_set.html +++ /dev/null @@ -1,23 +0,0 @@ - -{% extends "account/base.html" %} - -{% load i18n crispy_forms_tags %} - -{% block head_title %}{% trans "Set Password" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Set Password" %}

- -
- {% csrf_token %} - {{ form|crispy }} - -
-
-
-
-{% endblock %} - diff --git a/api/funkwhale_api/templates/account/signup.html b/api/funkwhale_api/templates/account/signup.html deleted file mode 100644 index 5db1cac77..000000000 --- a/api/funkwhale_api/templates/account/signup.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load crispy_forms_tags %} - -{% block title %}{% trans "Signup" %}{% endblock title %} - -{% block content %} - -
-
-
-

{% trans "Sign Up" %}

- -

{% blocktrans %}Already have an account? Then please sign in.{% endblocktrans %}

- - -
-
-
- - -{% endblock content %} - - - diff --git a/api/funkwhale_api/templates/account/signup_closed.html b/api/funkwhale_api/templates/account/signup_closed.html deleted file mode 100644 index 24e93d7bc..000000000 --- a/api/funkwhale_api/templates/account/signup_closed.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} - -{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Sign Up Closed" %}

- -

{% trans "We are sorry, but the sign up is currently closed." %}

-
-
-
-{% endblock %} - diff --git a/api/funkwhale_api/templates/account/verification_sent.html b/api/funkwhale_api/templates/account/verification_sent.html deleted file mode 100644 index f994b46e0..000000000 --- a/api/funkwhale_api/templates/account/verification_sent.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} - -{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Verify Your E-mail Address" %}

- -

{% blocktrans %}We have sent an e-mail to {{ email }} for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

-
-
-
- -{% endblock %} diff --git a/api/funkwhale_api/templates/account/verified_email_required.html b/api/funkwhale_api/templates/account/verified_email_required.html deleted file mode 100644 index bf4887a4a..000000000 --- a/api/funkwhale_api/templates/account/verified_email_required.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} - -{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Verify Your E-mail Address" %}

- - {% url 'account_email' as email_url %} - -

{% blocktrans %}This part of the site requires us to verify that - you are who you claim to be. For this purpose, we require that you - verify ownership of your e-mail address. {% endblocktrans %}

- -

{% blocktrans %}We have sent an e-mail to you for - verification. Please click on the link inside this e-mail. Please - contact us if you do not receive it within a few minutes.{% endblocktrans %}

- -

{% blocktrans %}Note: you can still change your e-mail address.{% endblocktrans %}

-
-
-
-{% endblock %} - diff --git a/api/funkwhale_api/templates/users/user_detail.html b/api/funkwhale_api/templates/users/user_detail.html deleted file mode 100644 index f8deb6b72..000000000 --- a/api/funkwhale_api/templates/users/user_detail.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}User: {{ object.username }}{% endblock %} - -{% block content %} -
- -
-
- -

{{ object.username }}

- {% if object.name %} -

{{ object.name }}

- {% endif %} -
-
- -{% if object == request.user %} - -
- -
- My Info - E-Mail - -
- -
- -{% endif %} - - -
-{% endblock content %} - diff --git a/api/funkwhale_api/templates/users/user_form.html b/api/funkwhale_api/templates/users/user_form.html deleted file mode 100644 index c07b8f2fc..000000000 --- a/api/funkwhale_api/templates/users/user_form.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base.html" %} -{% load crispy_forms_tags %} - -{% block title %}{{ user.username }}{% endblock %} - -{% block content %} -

{{ user.username }}

-
- {% csrf_token %} - {{ form|crispy }} -
-
- -
-
-
-{% endblock %} diff --git a/api/funkwhale_api/templates/users/user_list.html b/api/funkwhale_api/templates/users/user_list.html deleted file mode 100644 index 7fbcc52d4..000000000 --- a/api/funkwhale_api/templates/users/user_list.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} -{% load static %}{% load i18n %} -{% block title %}Members{% endblock %} - -{% block content %} - -
- -

Users

- -
- {% for user in user_list %} - -

{{ user.username }}

-
- {% endfor %} - -
- -{% endblock content %} diff --git a/api/funkwhale_api/users/tests/test_views.py b/api/funkwhale_api/users/tests/test_views.py index 5f8233bc6..6250a7ca7 100644 --- a/api/funkwhale_api/users/tests/test_views.py +++ b/api/funkwhale_api/users/tests/test_views.py @@ -42,7 +42,7 @@ class UserTestCase(TestCase): self.assertEqual(response.status_code, 403) def test_can_fetch_data_from_api(self): - url = self.reverse('api:users:users-me') + url = self.reverse('api:v1:users:users-me') response = self.client.get(url) # login required self.assertEqual(response.status_code, 401) diff --git a/api/funkwhale_api/users/urls.py b/api/funkwhale_api/users/urls.py deleted file mode 100644 index 94b18b264..000000000 --- a/api/funkwhale_api/users/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -from django.conf.urls import url - -from . import views - -urlpatterns = [ - -] diff --git a/api/pytest.ini b/api/pytest.ini new file mode 100644 index 000000000..4ab907403 --- /dev/null +++ b/api/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +DJANGO_SETTINGS_MODULE=config.settings.test + +# -- recommended but optional: +python_files = tests.py test_*.py *_tests.py diff --git a/api/requirements/base.txt b/api/requirements/base.txt index 626525572..ae851962a 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -1,17 +1,11 @@ # Bleeding edge Django -django==1.8.7 +django==1.11 # Configuration django-environ==0.4.0 django-secure==1.0.1 whitenoise==2.0.6 - -# Forms -django-braces==1.8.1 -# django-crispy-forms==1.5.2 -# django-floppyforms==1.5.2 - # Models django-model-utils==2.3.1 @@ -26,10 +20,6 @@ django-allauth==0.24.1 # Python-PostgreSQL Database Adapter psycopg2==2.6.1 -# Unicode slugification -unicode-slugify==0.1.3 -django-autoslug==1.9.3 - # Time zones support pytz==2015.7 @@ -42,21 +32,21 @@ celery==3.1.19 # Your custom requirements go here -django-cors-headers -musicbrainzngs +django-cors-headers==2.1.0 +musicbrainzngs==0.6 youtube_dl>=2015.12.21 -djangorestframework -djangorestframework-jwt -django-celery -django-mptt -google-api-python-client -arrow -django-taggit -persisting_theory -django-versatileimagefield -django-cachalot -django-rest-auth -beautifulsoup4 -markdown -ipython -mutagen +djangorestframework==3.6.3 +djangorestframework-jwt==1.11.0 +django-celery==3.2.1 +django-mptt==0.8.7 +google-api-python-client==1.6.2 +arrow==0.10.0 +django-taggit==0.22.1 +persisting-theory==0.2.1 +django-versatileimagefield==1.7.1 +django-cachalot==1.5.0 +django-rest-auth==0.9.1 +beautifulsoup4==4.6.0 +Markdown==2.6.8 +ipython==6.1.0 +mutagen==1.38 diff --git a/api/requirements/local.txt b/api/requirements/local.txt index ff8f35c74..3f681b26d 100644 --- a/api/requirements/local.txt +++ b/api/requirements/local.txt @@ -2,11 +2,11 @@ -r base.txt coverage==4.0.3 django_coverage_plugin==1.1 -Sphinx +Sphinx==1.6.2 django-extensions==1.5.9 Werkzeug==0.11.2 django-test-plus==1.0.11 -factory_boy==2.6.0 +factory_boy>=2.8.1 # django-debug-toolbar that works with Django 1.5+ django-debug-toolbar>=1.5,<1.6 diff --git a/api/requirements/test.txt b/api/requirements/test.txt index bcb6ef060..a26cf5bdb 100644 --- a/api/requirements/test.txt +++ b/api/requirements/test.txt @@ -1,12 +1,11 @@ # Test dependencies go here. --r base.txt +-r local.txt - -coverage==4.0.3 -django_coverage_plugin==1.1 flake8==2.5.0 -django-test-plus==1.0.11 -factory_boy>=2.8.1 -model_mommy -tox +model-mommy==1.3.2 +tox==2.7.0 +pytest +pytest-django +pytest-sugar +pytest-xdist diff --git a/api/runtests b/api/runtests index bd8db7a84..48e7b8267 100755 --- a/api/runtests +++ b/api/runtests @@ -2,4 +2,4 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -docker-compose -f $DIR/test.yml run test python manage.py test "$@" +docker-compose -f $DIR/test.yml run test pytest "$@" diff --git a/api/test.yml b/api/test.yml index dc50a9b54..6215e27de 100644 --- a/api/test.yml +++ b/api/test.yml @@ -1,8 +1,6 @@ test: dockerfile: docker/Dockerfile.test build: . - command: python manage.py test + command: pytest volumes: - .:/app - environment: - - DJANGO_SETTINGS_MODULE=config.settings.test diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index 38d355cb1..fd3b4327f 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -30,7 +30,7 @@ DJANGO_SETTINGS_MODULE=config.settings.production DJANGO_SECRET_KEY= # You don't have to edit this -DJANGO_ADMIN_URL=^admin/ +DJANGO_ADMIN_URL=^api/admin/ # Update it to match the domain that will be used to reach your funkwhale # instance diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 0b2a534df..7395e37d9 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -40,7 +40,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; proxy_pass http://funkwhale-api/api/; } @@ -48,6 +48,6 @@ server { alias /srv/funkwhale/data/media/; } location /staticfiles/ { - alias /srv/funkwhale/data/staticfiles/; + alias /srv/funkwhale/data/static/; } } diff --git a/dev.yml b/dev.yml index 526ce1ba8..21b0912e3 100644 --- a/dev.yml +++ b/dev.yml @@ -36,17 +36,18 @@ services: - C_FORCE_ROOT=true volumes: - ./api:/app - + - ./data/music:/music api: env_file: .env.dev build: context: ./api - dockerfile: docker/Dockerfile.local + dockerfile: docker/Dockerfile.test command: python /app/manage.py runserver 0.0.0.0:12081 volumes: - ./api:/app + - ./data/music:/music ports: - - "12081" + - "12081:12081" links: - postgres - redis diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 000000000..c4092dc8b --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,10 @@ +Changelog +========= + + +0.1 +------- + +2017-06-26 + +Initial release diff --git a/docs/importing-music.rst b/docs/importing-music.rst index 29f33ffc7..5fdff7e93 100644 --- a/docs/importing-music.rst +++ b/docs/importing-music.rst @@ -14,14 +14,18 @@ least an ``artist``, ``album`` and ``title`` tag, you can import those tracks as docker-compose run --rm api python manage.py import_files "/music/**/*.ogg" --recursive --noinput +For the best results, we recommand tagging your music collection through +`Picard `_ in order to have the best quality metadata. + .. note:: This command is idempotent, meaning you can run it multiple times on the same files and already imported files will simply be skipped. -.. warning:: +.. note:: + + At the moment, only OGG/Vorbis and MP3 files with ID3 tags are supported - At the moment, only ogg files are supported. MP3 support will be implemented soon. Getting demo tracks ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index b4c4119bf..ca6504b52 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ Funkwhale is a self-hosted, modern free and open-source music server, heavily in features installation/index importing-music - + changelog Indices and tables ================== diff --git a/front/src/config.js b/front/src/config.js index 03e7a1821..5bc5c160e 100644 --- a/front/src/config.js +++ b/front/src/config.js @@ -4,7 +4,7 @@ class Config { if (!this.BACKEND_URL.endsWith('/')) { this.BACKEND_URL += '/' } - this.API_URL = this.BACKEND_URL + 'api/' + this.API_URL = this.BACKEND_URL + 'api/v1/' } }