Merge branch 'release/0.1'

This commit is contained in:
Eliot Berriot 2017-06-26 22:15:05 +02:00
commit 8335b1acc1
53 changed files with 352 additions and 695 deletions

View File

@ -1,3 +1,3 @@
BACKEND_URL=http://localhost:6001 BACKEND_URL=http://localhost:12081
YOUTUBE_API_KEY= YOUTUBE_API_KEY=
API_AUTHENTICATION_REQUIRED=False API_AUTHENTICATION_REQUIRED=False

View File

@ -1,18 +1,26 @@
variables:
IMAGE_NAME: funkwhale/funkwhale
IMAGE: $IMAGE_NAME:$CI_BUILD_REF
IMAGE_LATEST: $IMAGE_NAME:latest
stages: stages:
- test - test
- build - build
- deploy
test_api: test_api:
stage: test stage: test
image: funkwhale/funkwhale:base
before_script: before_script:
- docker-compose -f api/test.yml build - cd api
- pip install -r requirements/test.txt
script: script:
- docker-compose -f api/test.yml run test - pytest
after_script:
- docker-compose -f api/test.yml run test rm -rf funkwhale_api/media/
tags: tags:
- dind - docker
build_front: build_front:
stage: build stage: build
@ -53,6 +61,33 @@ pages:
paths: paths:
- public - public
only: only:
- master - develop
tags: tags:
- docker - 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

View File

@ -2,6 +2,8 @@ from rest_framework import routers
from django.conf.urls import include, url from django.conf.urls import include, url
from funkwhale_api.music import views from funkwhale_api.music import views
from funkwhale_api.playlists import views as playlists_views from funkwhale_api.playlists import views as playlists_views
from rest_framework_jwt import views as jwt_views
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register(r'tags', views.TagViewSet, 'tags') 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'submit', views.SubmitViewSet, 'submit')
router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists') router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists')
router.register(r'playlist-tracks', playlists_views.PlaylistTrackViewSet, 'playlist-tracks') 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'^providers/', include('funkwhale_api.providers.urls', namespace='providers')),
url(r'^favorites/', include('funkwhale_api.favorites.urls', namespace='favorites')), url(r'^favorites/', include('funkwhale_api.favorites.urls', namespace='favorites')),
url(r'^search$', views.Search.as_view(), name='search'), url(r'^search$', views.Search.as_view(), name='search'),
url(r'^radios/', include('funkwhale_api.radios.urls', namespace='radios')), url(r'^radios/', include('funkwhale_api.radios.urls', namespace='radios')),
url(r'^history/', include('funkwhale_api.history.urls', namespace='history')), url(r'^history/', include('funkwhale_api.history.urls', namespace='history')),
url(r'^users/', include('funkwhale_api.users.api_urls', namespace='users')), url(r'^users/', include('funkwhale_api.users.api_urls', namespace='users')),
url(r'^token/', 'rest_framework_jwt.views.obtain_jwt_token'), url(r'^token/', jwt_views.obtain_jwt_token),
url(r'^token/refresh/', 'rest_framework_jwt.views.refresh_jwt_token'), url(r'^token/refresh/', jwt_views.refresh_jwt_token),
]
urlpatterns = [
url(r'^v1/', include(v1_patterns, namespace='v1'))
] ]

View File

@ -199,7 +199,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap3'
STATIC_ROOT = str(ROOT_DIR('staticfiles')) STATIC_ROOT = str(ROOT_DIR('staticfiles'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url # 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 # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = ( STATICFILES_DIRS = (

View File

@ -24,7 +24,7 @@ class TestFavorites(TestCase):
def test_user_can_get_his_favorites(self): def test_user_can_get_his_favorites(self):
favorite = TrackFavorite.add(self.track, self.user) 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') self.client.login(username=self.user.username, password='test')
response = self.client.get(url) response = self.client.get(url)
@ -41,7 +41,7 @@ class TestFavorites(TestCase):
self.assertEqual(expected, parsed_json['results']) self.assertEqual(expected, parsed_json['results'])
def test_user_can_add_favorite_via_api(self): 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') self.client.login(username=self.user.username, password='test')
response = self.client.post(url, {'track': self.track.pk}) 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): def test_user_can_remove_favorite_via_api(self):
favorite = TrackFavorite.add(self.track, self.user) 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') self.client.login(username=self.user.username, password='test')
response = self.client.delete(url, {'track': self.track.pk}) response = self.client.delete(url, {'track': self.track.pk})
self.assertEqual(response.status_code, 204) 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): def test_user_can_remove_favorite_via_api_using_track_id(self):
favorite = TrackFavorite.add(self.track, self.user) 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') self.client.login(username=self.user.username, password='test')
response = self.client.delete( response = self.client.delete(
url, json.dumps({'track': self.track.pk}), url, json.dumps({'track': self.track.pk}),
@ -83,7 +83,7 @@ class TestFavorites(TestCase):
def test_can_restrict_api_views_to_authenticated_users(self): def test_can_restrict_api_views_to_authenticated_users(self):
urls = [ urls = [
('api:favorites:tracks-list', 'get'), ('api:v1:favorites:tracks-list', 'get'),
] ]
for route_name, method in urls: for route_name, method in urls:
@ -103,7 +103,7 @@ class TestFavorites(TestCase):
def test_can_filter_tracks_by_favorites(self): def test_can_filter_tracks_by_favorites(self):
favorite = TrackFavorite.add(self.track, self.user) 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') self.client.login(username=self.user.username, password='test')
response = self.client.get(url, data={'favorites': True}) response = self.client.get(url, data={'favorites': True})

View File

@ -23,7 +23,7 @@ class TestHistory(TestCase):
def test_anonymous_user_can_create_listening_via_api(self): def test_anonymous_user_can_create_listening_via_api(self):
track = mommy.make('music.Track') 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, { response = self.client.post(url, {
'track': track.pk, 'track': track.pk,
}) })
@ -38,7 +38,7 @@ class TestHistory(TestCase):
self.client.login(username=self.user.username, password='test') 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, { response = self.client.post(url, {
'track': track.pk, 'track': track.pk,
}) })

View File

@ -1,34 +1,130 @@
import mutagen import mutagen
import arrow
NODEFAULT = object() NODEFAULT = object()
class Metadata(object):
ALIASES = { class TagNotFound(KeyError):
'release': 'musicbrainz_albumid', pass
'artist': 'musicbrainz_artistid',
'recording': 'musicbrainz_trackid',
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): def __init__(self, path):
self._file = mutagen.File(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: try:
v = self._file[key] getter = field_conf.get('getter', self._conf['getter'])
v = getter(self._file, real_key)
except KeyError: except KeyError:
if default == NODEFAULT: if default == NODEFAULT:
raise raise TagNotFound(real_key)
return default return default
# Some tags are returned as lists of string converter = field_conf.get('to_application')
if single: if converter:
return v[0] v = converter(v)
return 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)

View File

@ -314,7 +314,7 @@ class Track(APIModelMixin):
return work return work
def get_lyrics_url(self): 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 @property
def full_name(self): def full_name(self):

Binary file not shown.

View File

@ -20,7 +20,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
def test_can_submit_youtube_url_for_track_import(self, *mocks): def test_can_submit_youtube_url_for_track_import(self, *mocks):
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed' mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
video_id = 'tPEE9ZwTmy0' 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') user = User.objects.create_superuser(username='test', email='test@test.com', password='test')
self.client.login(username=user.username, 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}) 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') user = User.objects.create_superuser(username='test', email='test@test.com', password='test')
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed' mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
video_id = 'tPEE9ZwTmy0' video_id = 'tPEE9ZwTmy0'
url = reverse('api:submit-single') url = reverse('api:v1:submit-single')
self.client.login(username=user.username, password='test') self.client.login(username=user.username, password='test')
with self.settings(CELERY_ALWAYS_EAGER=False): 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}) 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') self.client.login(username=user.username, password='test')
with self.settings(CELERY_ALWAYS_EAGER=False): with self.settings(CELERY_ALWAYS_EAGER=False):
response = self.client.post(url, json.dumps(payload), content_type="application/json") 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') self.client.login(username=user.username, password='test')
with self.settings(CELERY_ALWAYS_EAGER=False): with self.settings(CELERY_ALWAYS_EAGER=False):
response = self.client.post(url, json.dumps(payload), content_type="application/json") 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) batch = models.ImportBatch.objects.create(submitted_by=user1)
job = models.ImportJob.objects.create(batch=batch, mbid=mbid, source=source) 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') self.client.login(username=user2.username, password='test')
response2 = self.client.get(url) response2 = self.client.get(url)
@ -175,7 +175,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
artist2 = models.Artist.objects.create(name='Test2') artist2 = models.Artist.objects.create(name='Test2')
query = 'test1' query = 'test1'
expected = '[{0}]'.format(json.dumps(serializers.ArtistSerializerNested(artist1).data)) 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)) response = self.client.get(url + '?query={0}'.format(query))
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8'))) 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") track2 = models.Track.objects.create(artist=artist2, title="test_track2")
query = 'test track 1' query = 'test track 1'
expected = '[{0}]'.format(json.dumps(serializers.TrackSerializerNested(track1).data)) 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)) response = self.client.get(url + '?query={0}'.format(query))
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8'))) self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
def test_can_restrict_api_views_to_authenticated_users(self): def test_can_restrict_api_views_to_authenticated_users(self):
urls = [ urls = [
('api:tags-list', 'get'), ('api:v1:tags-list', 'get'),
('api:tracks-list', 'get'), ('api:v1:tracks-list', 'get'),
('api:artists-list', 'get'), ('api:v1:artists-list', 'get'),
('api:albums-list', 'get'), ('api:v1:albums-list', 'get'),
] ]
for route_name, method in urls: for route_name, method in urls:

View File

@ -59,7 +59,7 @@ Is it me you're looking for?"""
work=None, work=None,
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448') 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( user = User.objects.create_user(
username='test', email='test@test.com', password='test') username='test', email='test@test.com', password='test')
self.client.login(username=user.username, password='test') self.client.login(username=user.username, password='test')

View File

@ -1,5 +1,6 @@
import unittest import unittest
import os import os
import datetime
from test_plus.test import TestCase from test_plus.test import TestCase
from funkwhale_api.music import metadata from funkwhale_api.music import metadata
@ -8,20 +9,72 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__))
class TestMetadata(TestCase): 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') path = os.path.join(DATA_DIR, 'test.ogg')
data = metadata.Metadata(path) 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( self.assertEqual(
data.get('musicbrainz_albumid'), data.get('musicbrainz_albumid'),
'a766da8b-8336-47aa-a3ee-371cc41ccc75') 'a766da8b-8336-47aa-a3ee-371cc41ccc75')
self.assertEqual( self.assertEqual(
data.get('musicbrainz_trackid'), data.get('musicbrainz_recordingid'),
'bd21ac48-46d8-4e78-925f-d9cc2a294656') 'bd21ac48-46d8-4e78-925f-d9cc2a294656')
self.assertEqual( self.assertEqual(
data.get('musicbrainz_artistid'), data.get('musicbrainz_artistid'),
'013c8e5b-d72a-4cd3-8dee-6c64d6125823') '013c8e5b-d72a-4cd3-8dee-6c64d6125823')
self.assertEqual(data.release, data.get('musicbrainz_albumid')) def test_can_get_metadata_from_id3_mp3_file(self, *mocks):
self.assertEqual(data.artist, data.get('musicbrainz_artistid')) path = os.path.join(DATA_DIR, 'test.mp3')
self.assertEqual(data.recording, data.get('musicbrainz_trackid')) 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')

View File

@ -121,13 +121,14 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
class TagViewSet(viewsets.ReadOnlyModelViewSet): class TagViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Tag.objects.all() queryset = Tag.objects.all().order_by('name')
serializer_class = serializers.TagSerializer serializer_class = serializers.TagSerializer
permission_classes = [ConditionalAuthentication] permission_classes = [ConditionalAuthentication]
class Search(views.APIView): class Search(views.APIView):
max_results = 3 max_results = 3
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
query = request.GET['query'] query = request.GET['query']
results = { results = {

View File

@ -13,7 +13,7 @@ class TestAPI(TestCase):
return_value=api_data.recordings['search']['brontide matador']) return_value=api_data.recordings['search']['brontide matador'])
def test_can_search_recording_in_musicbrainz_api(self, *mocks): def test_can_search_recording_in_musicbrainz_api(self, *mocks):
query = 'brontide matador' 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'] expected = api_data.recordings['search']['brontide matador']
response = self.client.get(url, data={'query': query}) response = self.client.get(url, data={'query': query})
@ -24,7 +24,7 @@ class TestAPI(TestCase):
return_value=api_data.releases['search']['brontide matador']) return_value=api_data.releases['search']['brontide matador'])
def test_can_search_release_in_musicbrainz_api(self, *mocks): def test_can_search_release_in_musicbrainz_api(self, *mocks):
query = 'brontide matador' 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'] expected = api_data.releases['search']['brontide matador']
response = self.client.get(url, data={'query': query}) response = self.client.get(url, data={'query': query})
@ -35,7 +35,7 @@ class TestAPI(TestCase):
return_value=api_data.artists['search']['lost fingers']) return_value=api_data.artists['search']['lost fingers'])
def test_can_search_artists_in_musicbrainz_api(self, *mocks): def test_can_search_artists_in_musicbrainz_api(self, *mocks):
query = 'lost fingers' 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'] expected = api_data.artists['search']['lost fingers']
response = self.client.get(url, data={'query': query}) response = self.client.get(url, data={'query': query})
@ -46,7 +46,7 @@ class TestAPI(TestCase):
return_value=api_data.artists['get']['lost fingers']) return_value=api_data.artists['get']['lost fingers'])
def test_can_get_artist_in_musicbrainz_api(self, *mocks): def test_can_get_artist_in_musicbrainz_api(self, *mocks):
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9' 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, 'uuid': uuid,
}) })
response = self.client.get(url) response = self.client.get(url)
@ -60,7 +60,7 @@ class TestAPI(TestCase):
def test_can_broswe_release_group_using_musicbrainz_api(self, *mocks): def test_can_broswe_release_group_using_musicbrainz_api(self, *mocks):
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9' uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
url = reverse( url = reverse(
'api:providers:musicbrainz:release-group-browse', 'api:v1:providers:musicbrainz:release-group-browse',
kwargs={ kwargs={
'artist_uuid': uuid, 'artist_uuid': uuid,
} }
@ -76,7 +76,7 @@ class TestAPI(TestCase):
def test_can_broswe_releases_using_musicbrainz_api(self, *mocks): def test_can_broswe_releases_using_musicbrainz_api(self, *mocks):
uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1' uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1'
url = reverse( url = reverse(
'api:providers:musicbrainz:release-browse', 'api:v1:providers:musicbrainz:release-browse',
kwargs={ kwargs={
'release_group_uuid': uuid, 'release_group_uuid': uuid,
} }

View File

@ -38,7 +38,7 @@ class TestPlayLists(TestCase):
def test_can_create_playlist_via_api(self): def test_can_create_playlist_via_api(self):
self.client.login(username=self.user.username, password='test') self.client.login(username=self.user.username, password='test')
url = reverse('api:playlists-list') url = reverse('api:v1:playlists-list')
data = { data = {
'name': 'test', 'name': 'test',
} }
@ -54,7 +54,7 @@ class TestPlayLists(TestCase):
self.client.login(username=self.user.username, password='test') self.client.login(username=self.user.username, password='test')
url = reverse('api:playlist-tracks-list') url = reverse('api:v1:playlist-tracks-list')
data = { data = {
'playlist': playlist.pk, 'playlist': playlist.pk,
'track': tracks[0].pk 'track': tracks[0].pk

View File

@ -9,41 +9,34 @@ from funkwhale_api.music import models, metadata
@celery.app.task(name='audiofile.from_path') @celery.app.task(name='audiofile.from_path')
def from_path(path): def from_path(path):
data = metadata.Metadata(path) data = metadata.Metadata(path)
artist = models.Artist.objects.get_or_create( artist = models.Artist.objects.get_or_create(
name__iexact=data.get('artist'), name__iexact=data.get('artist'),
defaults={'name': data.get('artist')}, defaults={
'name': data.get('artist'),
'mbid': data.get('musicbrainz_artistid', None),
},
)[0] )[0]
release_date = None release_date = data.get('date', default=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( album = models.Album.objects.get_or_create(
title__iexact=data.get('album'), title__iexact=data.get('album'),
artist=artist, artist=artist,
defaults={ defaults={
'title': data.get('album'), 'title': data.get('album'),
'release_date': release_date, 'release_date': release_date,
'mbid': data.get('musicbrainz_albumid', None),
}, },
)[0] )[0]
position = None position = data.get('track_number', default=None)
try:
position = int(data.get('tracknumber', None))
except ValueError:
pass
track = models.Track.objects.get_or_create( track = models.Track.objects.get_or_create(
title__iexact=data.get('title'), title__iexact=data.get('title'),
album=album, album=album,
defaults={ defaults={
'title': data.get('title'), 'title': data.get('title'),
'position': position, 'position': position,
'mbid': data.get('musicbrainz_recordingid', None),
}, },
)[0] )[0]

View File

@ -14,21 +14,37 @@ class TestAudioFile(TestCase):
'artist': ['Test artist'], 'artist': ['Test artist'],
'album': ['Test album'], 'album': ['Test album'],
'title': ['Test track'], 'title': ['Test track'],
'tracknumber': ['4'], 'TRACKNUMBER': ['4'],
'date': ['2012-08-15'] '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( track_file = importer.from_path(
os.path.join(DATA_DIR, 'dummy_file.ogg')) os.path.join(DATA_DIR, 'dummy_file.ogg'))
self.assertEqual( self.assertEqual(
track_file.track.title, metadata['title'][0]) track_file.track.title, metadata['title'][0])
self.assertEqual(
track_file.track.mbid, metadata['musicbrainz_trackid'][0])
self.assertEqual( self.assertEqual(
track_file.track.position, 4) track_file.track.position, 4)
self.assertEqual( self.assertEqual(
track_file.track.album.title, metadata['album'][0]) track_file.track.album.title, metadata['album'][0])
self.assertEqual(
track_file.track.album.mbid,
metadata['musicbrainz_albumid'][0])
self.assertEqual( self.assertEqual(
track_file.track.album.release_date, datetime.date(2012, 8, 15)) track_file.track.album.release_date, datetime.date(2012, 8, 15))
self.assertEqual( self.assertEqual(
track_file.track.artist.name, metadata['artist'][0]) track_file.track.artist.name, metadata['artist'][0])
self.assertEqual(
track_file.track.artist.mbid,
metadata['musicbrainz_artistid'][0])

View File

@ -26,7 +26,7 @@ class TestAPI(TestCase):
def test_can_get_search_results_from_funkwhale(self, *mocks): def test_can_get_search_results_from_funkwhale(self, *mocks):
query = '8 bit adventure' query = '8 bit adventure'
expected = json.dumps(client.search(query)) 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)) response = self.client.get(url + '?query={0}'.format(query))
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8'))) self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
@ -67,7 +67,7 @@ class TestAPI(TestCase):
} }
expected = json.dumps(client.search_multiple(queries)) 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( response = self.client.post(
url, json.dumps(queries), content_type='application/json') url, json.dumps(queries), content_type='application/json')

View File

@ -94,7 +94,7 @@ class TestRadios(TestCase):
self.assertEqual(radio.session, restarted_radio.session) self.assertEqual(radio.session, restarted_radio.session)
def test_can_get_start_radio_from_api(self): 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'}) response = self.client.post(url, {'radio_type': 'random'})
session = models.RadioSession.objects.latest('id') session = models.RadioSession.objects.latest('id')
self.assertEqual(session.radio_type, 'random') self.assertEqual(session.radio_type, 'random')
@ -107,7 +107,7 @@ class TestRadios(TestCase):
self.assertEqual(session.user, self.user) self.assertEqual(session.user, self.user)
def test_can_start_radio_for_anonymous_user(self): 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'}) response = self.client.post(url, {'radio_type': 'random'})
session = models.RadioSession.objects.latest('id') session = models.RadioSession.objects.latest('id')
@ -118,11 +118,11 @@ class TestRadios(TestCase):
tracks = mommy.make('music.Track', _quantity=1) tracks = mommy.make('music.Track', _quantity=1)
self.client.login(username=self.user.username, password='test') 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'}) response = self.client.post(url, {'radio_type': 'random'})
session = models.RadioSession.objects.latest('id') 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}) response = self.client.post(url, {'session': session.pk})
data = json.loads(response.content.decode('utf-8')) data = json.loads(response.content.decode('utf-8'))
@ -173,7 +173,7 @@ class TestRadios(TestCase):
def test_can_start_artist_radio_from_api(self): def test_can_start_artist_radio_from_api(self):
artist = mommy.make('music.Artist') 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}) response = self.client.post(url, {'radio_type': 'artist', 'related_object_id': artist.id})
session = models.RadioSession.objects.latest('id') session = models.RadioSession.objects.latest('id')

View File

@ -1,2 +0,0 @@
{% extends "base.html" %}
{% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %}

View File

@ -1,80 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block head_title %}{% trans "Account" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "E-mail Addresses" %}</h2>
{% if user.emailaddress_set.all %}
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
<form action="{% url 'account_email' %}" class="email_list" method="post">
{% csrf_token %}
<fieldset class="blockLabels">
{% for emailaddress in user.emailaddress_set.all %}
<div class="ctrlHolder">
<label for="email_radio_{{ forloop.counter }}" class="{% if emailaddress.primary %}primary_email{% endif %}">
<input id="email_radio_{{ forloop.counter }}" type="radio" name="email" {% if emailaddress.primary %}checked="checked"{% endif %} value="{{ emailaddress.email }}"/>
{{ emailaddress.email }}
{% if emailaddress.verified %}
<span class="verified">{% trans "Verified" %}</span>
{% else %}
<span class="unverified">{% trans "Unverified" %}</span>
{% endif %}
{% if emailaddress.primary %}<span class="primary">{% trans "Primary" %}</span>{% endif %}
</label>
</div>
{% endfor %}
<div class="buttonHolder">
<button class="secondaryAction" type="submit" name="action_primary" >{% trans 'Make Primary' %}</button>
<button class="secondaryAction" type="submit" name="action_send" >{% trans 'Re-send Verification' %}</button>
<button class="primaryAction" type="submit" name="action_remove" >{% trans 'Remove' %}</button>
</div>
</fieldset>
</form>
{% else %}
<p><strong>{% trans 'Warning:'%}</strong> {% 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." %}</p>
{% endif %}
<h2>{% trans "Add E-mail Address" %}</h2>
<form method="post" action="." class="add_email">
{% csrf_token %}
{{ form|crispy }}
<button class="btn" name="action_add" type="submit">{% trans "Add E-mail" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_body %}
<script type="text/javascript">
(function() {
var message = "{% trans 'Do you really want to remove the selected e-mail address?' %}";
var actions = document.getElementsByName('action_remove');
if (actions.length) {
actions[0].addEventListener("click", function(e) {
if (! confirm(message)) {
e.preventDefault();
}
});
}
})();
</script>
{% endblock %}

View File

@ -1,37 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load account %}
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h2>{% trans "Confirm E-mail Address" %}</h2>
{% if confirmation %}
{% user_display confirmation.email_address.user as user_display %}
<p>{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a href="mailto:{{ email }}">{{ email }}</a> is an e-mail address for user {{ user_display }}.{% endblocktrans %}</p>
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
{% csrf_token %}
<button id="confirm-button" class="submit" type="submit">{% trans 'Confirm' %}</button>
</form>
{% else %}
{% url 'account_email' as email_url %}
<p>{% blocktrans %}This e-mail confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new e-mail confirmation request</a>.{% endblocktrans %}</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load account %}
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h2>{% trans "Confirm E-mail Address" %}</h2>
{% user_display email_address.user as user_display %}
<p>{% blocktrans with email_address.email as email %}You have confirmed that <a href="mailto:{{ email }}">{{ email }}</a> is an e-mail address for user {{ user_display }}.{% endblocktrans %}</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -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 %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Sign In" %}</h2>
{% get_providers as socialaccount_providers %}
{% if socialaccount_providers %}
<p>{% blocktrans with site.name as site_name %}Please sign in with one
of your existing third party accounts. Or, <a href="{{ signup_url }}">sign up</a>
for a {{ site_name }} account and sign in below:{% endblocktrans %}</p>
<div class="socialaccount_ballot">
<ul class="socialaccount_providers">
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
</ul>
<div class="login-or">{% trans 'or' %}</div>
</div>
{% include "socialaccount/snippets/login_extra.html" %}
{% endif %}
<form class="login" method="POST" action="{% url 'account_login' %}">
{% csrf_token %}
{{ form|crispy }}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<button id="sign-in-button" class="btn btn-primary" type="submit">{% trans "Sign In" %}</button>
<a class="button secondaryAction" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Sign Out" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Sign Out" %}</h2>
<p>{% trans 'Are you sure you want to sign out?' %}</p>
<form method="post" action="{% url 'account_logout' %}">
{% csrf_token %}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
{% endif %}
<button class="btn btn-danger" type="submit">{% trans 'Sign Out' %}</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Change Password" %}</h2>
<form method="POST" action="./" class="password_change">
{% csrf_token %}
{{ form|crispy }}
<button class="btn" type="submit" name="action">{% trans "Change Password" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -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 %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Password Reset" %}</h2>
{% if user.is_authenticated %}
{% include "account/snippets/already_logged_in.html" %}
{% endif %}
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}</p>
<form method="POST" action="./" class="password_reset">
{% csrf_token %}
{{ form|crispy }}
<button class="btn" type="submit">{% trans "Reset My Password" %}</button>
</form>
<p>{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}</p>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
{{ block.super }}
<script>
$("#id_email").focus();
</script>
{% endblock javascript %}

View File

@ -1,22 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load account %}
{% block head_title %}{% trans "Password Reset" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h2>{% trans "Password Reset" %}</h2>
{% if user.is_authenticated %}
{% include "account/snippets/already_logged_in.html" %}
{% endif %}
<p>{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,32 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h2>{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}</h2>
{% if token_fail %}
{% url 'account_reset_password' as passwd_reset_url %}
<p>{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a <a href="{{ passwd_reset_url }}">new password reset</a>.{% endblocktrans %}</p>
{% else %}
{% if form %}
<form method="POST" action="./">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" name="action">{% trans "change password" %}</button>
</form>
{% else %}
<p>{% trans 'Your password is now changed.' %}</p>
{% endif %}
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,16 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Change Password" %}</h2>
<p>{% trans 'Your password is now changed.' %}</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,23 +0,0 @@
{% extends "account/base.html" %}
{% load i18n crispy_forms_tags %}
{% block head_title %}{% trans "Set Password" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Set Password" %}</h2>
<form method="POST" action="./" class="password_set">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" name="action" value="{% trans "Set Password" %}"/>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,33 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% trans "Signup" %}{% endblock title %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h1>{% trans "Sign Up" %}</h1>
<p>{% blocktrans %}Already have an account? Then please <a href="{{ login_url }}">sign in</a>.{% endblocktrans %}</p>
<form class="signup" id="signup_form" method="post" action="{% url 'account_signup' %}">
{% csrf_token %}
{{ form|crispy }}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<button id="sign-up-button" class="btn btn-primary" type="submit">{% trans "Sign Up" %} &raquo;</button>
</form>
</div>
</div>
</div>
{% endblock content %}

View File

@ -1,18 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Sign Up Closed" %}</h2>
<p>{% trans "We are sorry, but the sign up is currently closed." %}</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Verify Your E-mail Address" %}</h2>
<p>{% blocktrans %}We have sent an e-mail to <a href="mailto:{{ email }}">{{ email }}</a> 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 %}</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<h2>{% trans "Verify Your E-mail Address" %}</h2>
{% url 'account_email' as email_url %}
<p>{% 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 %}</p>
<p>{% 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 %}</p>
<p>{% blocktrans %}<strong>Note:</strong> you can still <a href="{{ email_url }}">change your e-mail address</a>.{% endblocktrans %}</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,36 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block title %}User: {{ object.username }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<h2>{{ object.username }}</h2>
{% if object.name %}
<p>{{ object.name }}</p>
{% endif %}
</div>
</div>
{% if object == request.user %}
<!-- Action buttons -->
<div class="row">
<div class="col-sm-12 ">
<a class="btn btn-primary" href="{% url 'users:update' %}">My Info</a>
<a class="btn btn-primary" href="{% url 'account_email' %}">E-Mail</a>
<!-- Your Stuff: Custom user template urls -->
</div>
</div>
<!-- End Action buttons -->
{% endif %}
</div>
{% endblock content %}

View File

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block title %}{{ user.username }}{% endblock %}
{% block content %}
<h1>{{ user.username }}</h1>
<form class="form-horizontal" method="post" action="{% url 'users:update' %}">
{% csrf_token %}
{{ form|crispy }}
<div class="control-group">
<div class="controls">
<button type="submit" class="btn">Update</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends "base.html" %}
{% load static %}{% load i18n %}
{% block title %}Members{% endblock %}
{% block content %}
<div class="container">
<h2>Users</h2>
<div class="list-group">
{% for user in user_list %}
<a href="{% url 'users:detail' user.username %}" class="list-group-item">
<h4 class="list-group-item-heading">{{ user.username }}</h4>
</a>
{% endfor %}
</div>
{% endblock content %}

View File

@ -42,7 +42,7 @@ class UserTestCase(TestCase):
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_can_fetch_data_from_api(self): 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) response = self.client.get(url)
# login required # login required
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from . import views
urlpatterns = [
]

5
api/pytest.ini Normal file
View File

@ -0,0 +1,5 @@
[pytest]
DJANGO_SETTINGS_MODULE=config.settings.test
# -- recommended but optional:
python_files = tests.py test_*.py *_tests.py

View File

@ -1,17 +1,11 @@
# Bleeding edge Django # Bleeding edge Django
django==1.8.7 django==1.11
# Configuration # Configuration
django-environ==0.4.0 django-environ==0.4.0
django-secure==1.0.1 django-secure==1.0.1
whitenoise==2.0.6 whitenoise==2.0.6
# Forms
django-braces==1.8.1
# django-crispy-forms==1.5.2
# django-floppyforms==1.5.2
# Models # Models
django-model-utils==2.3.1 django-model-utils==2.3.1
@ -26,10 +20,6 @@ django-allauth==0.24.1
# Python-PostgreSQL Database Adapter # Python-PostgreSQL Database Adapter
psycopg2==2.6.1 psycopg2==2.6.1
# Unicode slugification
unicode-slugify==0.1.3
django-autoslug==1.9.3
# Time zones support # Time zones support
pytz==2015.7 pytz==2015.7
@ -42,21 +32,21 @@ celery==3.1.19
# Your custom requirements go here # Your custom requirements go here
django-cors-headers django-cors-headers==2.1.0
musicbrainzngs musicbrainzngs==0.6
youtube_dl>=2015.12.21 youtube_dl>=2015.12.21
djangorestframework djangorestframework==3.6.3
djangorestframework-jwt djangorestframework-jwt==1.11.0
django-celery django-celery==3.2.1
django-mptt django-mptt==0.8.7
google-api-python-client google-api-python-client==1.6.2
arrow arrow==0.10.0
django-taggit django-taggit==0.22.1
persisting_theory persisting-theory==0.2.1
django-versatileimagefield django-versatileimagefield==1.7.1
django-cachalot django-cachalot==1.5.0
django-rest-auth django-rest-auth==0.9.1
beautifulsoup4 beautifulsoup4==4.6.0
markdown Markdown==2.6.8
ipython ipython==6.1.0
mutagen mutagen==1.38

View File

@ -2,11 +2,11 @@
-r base.txt -r base.txt
coverage==4.0.3 coverage==4.0.3
django_coverage_plugin==1.1 django_coverage_plugin==1.1
Sphinx Sphinx==1.6.2
django-extensions==1.5.9 django-extensions==1.5.9
Werkzeug==0.11.2 Werkzeug==0.11.2
django-test-plus==1.0.11 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 that works with Django 1.5+
django-debug-toolbar>=1.5,<1.6 django-debug-toolbar>=1.5,<1.6

View File

@ -1,12 +1,11 @@
# Test dependencies go here. # Test dependencies go here.
-r base.txt -r local.txt
coverage==4.0.3
django_coverage_plugin==1.1
flake8==2.5.0 flake8==2.5.0
django-test-plus==1.0.11 model-mommy==1.3.2
factory_boy>=2.8.1 tox==2.7.0
model_mommy pytest
tox pytest-django
pytest-sugar
pytest-xdist

View File

@ -2,4 +2,4 @@
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 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 "$@"

View File

@ -1,8 +1,6 @@
test: test:
dockerfile: docker/Dockerfile.test dockerfile: docker/Dockerfile.test
build: . build: .
command: python manage.py test command: pytest
volumes: volumes:
- .:/app - .:/app
environment:
- DJANGO_SETTINGS_MODULE=config.settings.test

View File

@ -30,7 +30,7 @@ DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY= DJANGO_SECRET_KEY=
# You don't have to edit this # 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 # Update it to match the domain that will be used to reach your funkwhale
# instance # instance

View File

@ -40,7 +40,7 @@ server {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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_redirect off;
proxy_pass http://funkwhale-api/api/; proxy_pass http://funkwhale-api/api/;
} }
@ -48,6 +48,6 @@ server {
alias /srv/funkwhale/data/media/; alias /srv/funkwhale/data/media/;
} }
location /staticfiles/ { location /staticfiles/ {
alias /srv/funkwhale/data/staticfiles/; alias /srv/funkwhale/data/static/;
} }
} }

View File

@ -36,17 +36,18 @@ services:
- C_FORCE_ROOT=true - C_FORCE_ROOT=true
volumes: volumes:
- ./api:/app - ./api:/app
- ./data/music:/music
api: api:
env_file: .env.dev env_file: .env.dev
build: build:
context: ./api context: ./api
dockerfile: docker/Dockerfile.local dockerfile: docker/Dockerfile.test
command: python /app/manage.py runserver 0.0.0.0:12081 command: python /app/manage.py runserver 0.0.0.0:12081
volumes: volumes:
- ./api:/app - ./api:/app
- ./data/music:/music
ports: ports:
- "12081" - "12081:12081"
links: links:
- postgres - postgres
- redis - redis

10
docs/changelog.rst Normal file
View File

@ -0,0 +1,10 @@
Changelog
=========
0.1
-------
2017-06-26
Initial release

View File

@ -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 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 <http://picard.musicbrainz.org/>`_ in order to have the best quality metadata.
.. note:: .. note::
This command is idempotent, meaning you can run it multiple times on the same This command is idempotent, meaning you can run it multiple times on the same
files and already imported files will simply be skipped. 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 Getting demo tracks
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^

View File

@ -14,7 +14,7 @@ Funkwhale is a self-hosted, modern free and open-source music server, heavily in
features features
installation/index installation/index
importing-music importing-music
changelog
Indices and tables Indices and tables
================== ==================

View File

@ -4,7 +4,7 @@ class Config {
if (!this.BACKEND_URL.endsWith('/')) { if (!this.BACKEND_URL.endsWith('/')) {
this.BACKEND_URL += '/' this.BACKEND_URL += '/'
} }
this.API_URL = this.BACKEND_URL + 'api/' this.API_URL = this.BACKEND_URL + 'api/v1/'
} }
} }