diff --git a/CHANGELOG b/CHANGELOG index 283bcfcfbf686993a0fd27f1e60b1c24bdd308f5..edff0877ed06f180ec16dfcc861334313637b395 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,94 @@ This changelog is viewable on the web at https://docs.funkwhale.audio/changelog. .. towncrier +0.14.1 (2018-06-06) +------------------- + +Upgrade instructions are available at https://docs.funkwhale.audio/upgrading.html + +Enhancements: + +- Display server version in the footer (#270) +- fix_track_files will now update files with bad mimetype (and not only the one + with no mimetype) (#273) +- Huge performance boost (~x5 to x7) during CLI import that queries MusicBrainz + (#288) +- Removed alpha-state transcoding support (#271) + +Bugfixes: + +- Broken logging statement during import error (#274) +- Broken search bar on library home (#278) +- Do not crash when importing track with an artist that do not match the + release artist (#237) +- Do not crash when tag contains multiple uuids with a / separator (#267) +- Ensure we do not store bad mimetypes (such as application/x-empty) (#266) +- Fix broken "play all" button that played only 25 tracks (#281) +- Fixed broken track download modal (overflow and wrong URL) (#239) +- Removed hardcoded size limit in file upload widget (#275) + + +Documentation: + +- Added warning about _protected/music location in nginx configuration (#247) + + +Removed alpha-state transcoding (#271) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A few months ago, a basic transcoding feature was implemented. Due to the way +this feature was designed, it was slow, CPU intensive on the server side, +and very tightly coupled to the reverse-proxy configuration, preventing +it to work Apache2, for instance. It was also not compatible with Subsonic clients. + +Based on that, we're currently removing support for transcoding +**in its current state**. The work on a better designed transcoding feature +can be tracked in https://code.eliotberriot.com/funkwhale/funkwhale/issues/272. + +You don't have to do anything on your side, but you may want to remove +the now obsolete configuration from your reverse proxy file (nginx only):: + + # Remove those blocks: + + # transcode cache + proxy_cache_path /tmp/funkwhale-transcode levels=1:2 keys_zone=transcode:10m max_size=1g inactive=7d; + + # Transcoding logic and caching + location = /transcode-auth { + include /etc/nginx/funkwhale_proxy.conf; + # needed so we can authenticate transcode requests, but still + # cache the result + internal; + set $query ''; + # ensure we actually pass the jwt to the underlytin auth url + if ($request_uri ~* "[^\?]+\?(.*)$") { + set $query $1; + } + proxy_pass http://funkwhale-api/api/v1/trackfiles/viewable/?$query; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + } + + location /api/v1/trackfiles/transcode/ { + include /etc/nginx/funkwhale_proxy.conf; + # this block deals with authenticating and caching transcoding + # requests. Caching is heavily recommended as transcoding + # is a CPU intensive process. + auth_request /transcode-auth; + if ($args ~ (.*)jwt=[^&]*(.*)) { + set $cleaned_args $1$2; + } + proxy_cache_key "$scheme$request_method$host$uri$is_args$cleaned_args"; + proxy_cache transcode; + proxy_cache_valid 200 7d; + proxy_ignore_headers "Set-Cookie"; + proxy_hide_header "Set-Cookie"; + add_header X-Cache-Status $upstream_cache_status; + proxy_pass http://funkwhale-api; + } + # end of transcoding logic + + 0.14 (2018-06-02) ----------------- diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py index 0896aba8a73279d59aca4b8af0013dfab1ec2943..8b5b81ad4b070cafc4de189a439c46a8195efebd 100644 --- a/api/funkwhale_api/__init__.py +++ b/api/funkwhale_api/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = '0.14' +__version__ = '0.14.1' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) diff --git a/api/funkwhale_api/music/forms.py b/api/funkwhale_api/music/forms.py deleted file mode 100644 index e68ab73cc2b95030d6dfb284a10b8becbefc9a9e..0000000000000000000000000000000000000000 --- a/api/funkwhale_api/music/forms.py +++ /dev/null @@ -1,23 +0,0 @@ -from django import forms - -from . import models - - -class TranscodeForm(forms.Form): - FORMAT_CHOICES = [ - ('ogg', 'ogg'), - ('mp3', 'mp3'), - ] - - to = forms.ChoiceField(choices=FORMAT_CHOICES) - BITRATE_CHOICES = [ - (64, '64'), - (128, '128'), - (256, '256'), - ] - bitrate = forms.ChoiceField( - choices=BITRATE_CHOICES, required=False) - - track_file = forms.ModelChoiceField( - queryset=models.TrackFile.objects.exclude(audio_file__isnull=True) - ) diff --git a/api/funkwhale_api/music/management/commands/fix_track_files.py b/api/funkwhale_api/music/management/commands/fix_track_files.py index 9adc1b9bf1a9d24f3cb26b6da4dce68dc6078213..c18e2b255a6842d4dc998551b0b1b55acbbac5cf 100644 --- a/api/funkwhale_api/music/management/commands/fix_track_files.py +++ b/api/funkwhale_api/music/management/commands/fix_track_files.py @@ -33,9 +33,9 @@ class Command(BaseCommand): def fix_mimetypes(self, dry_run, **kwargs): self.stdout.write('Fixing missing mimetypes...') matching = models.TrackFile.objects.filter( - source__startswith='file://', mimetype=None) + source__startswith='file://').exclude(mimetype__startswith='audio/') self.stdout.write( - '[mimetypes] {} entries found with no mimetype'.format( + '[mimetypes] {} entries found with bad or no mimetype'.format( matching.count())) for extension, mimetype in utils.EXTENSION_TO_MIMETYPE.items(): qs = matching.filter(source__endswith='.{}'.format(extension)) diff --git a/api/funkwhale_api/music/metadata.py b/api/funkwhale_api/music/metadata.py index 3637b1c8c5d78311dd2e3975cd02bd48d6f9269e..4c17c42c0d51ac9c92501d43ecbf1aab3de0377f 100644 --- a/api/funkwhale_api/music/metadata.py +++ b/api/funkwhale_api/music/metadata.py @@ -91,10 +91,23 @@ def convert_track_number(v): pass + +class FirstUUIDField(forms.UUIDField): + def to_python(self, value): + try: + # sometimes, Picard leaves to uuids in the field, separated + # by a slash + value = value.split('/')[0] + except (AttributeError, IndexError, TypeError): + pass + + return super().to_python(value) + + VALIDATION = { - 'musicbrainz_artistid': forms.UUIDField(), - 'musicbrainz_albumid': forms.UUIDField(), - 'musicbrainz_recordingid': forms.UUIDField(), + 'musicbrainz_artistid': FirstUUIDField(), + 'musicbrainz_albumid': FirstUUIDField(), + 'musicbrainz_recordingid': FirstUUIDField(), } CONF = { diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 0ba4d22c339f3798945d069c5aff801657cee5dc..bf3f9e12c29ebbada7e158a57a37b755aafecb1d 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -334,6 +334,11 @@ class TrackQuerySet(models.QuerySet): .prefetch_related('files')) +def get_artist(release_list): + return Artist.get_or_create_from_api( + mbid=release_list[0]['artist-credits'][0]['artists']['id'])[0] + + class Track(APIModelMixin): title = models.CharField(max_length=255) artist = models.ForeignKey( @@ -363,8 +368,9 @@ class Track(APIModelMixin): 'musicbrainz_field_name': 'title' }, 'artist': { - 'musicbrainz_field_name': 'artist-credit', - 'converter': lambda v: Artist.get_or_create_from_api(mbid=v[0]['artist']['id'])[0], + # we use the artist from the release to avoid #237 + 'musicbrainz_field_name': 'release-list', + 'converter': get_artist, }, 'album': { 'musicbrainz_field_name': 'release-list', @@ -431,7 +437,40 @@ class Track(APIModelMixin): title__iexact=title, defaults=kwargs) - + @classmethod + def get_or_create_from_release(cls, release_mbid, mbid): + release_mbid = str(release_mbid) + mbid = str(mbid) + try: + return cls.objects.get(mbid=mbid), False + except cls.DoesNotExist: + pass + + album = Album.get_or_create_from_api(release_mbid)[0] + data = musicbrainz.client.api.releases.get( + str(album.mbid), includes=Album.api_includes) + tracks = [ + t + for m in data['release']['medium-list'] + for t in m['track-list'] + ] + track_data = None + for track in tracks: + if track['recording']['id'] == mbid: + track_data = track + break + if not track_data: + raise ValueError('No track found matching this ID') + + return cls.objects.update_or_create( + mbid=mbid, + defaults={ + 'position': int(track['position']), + 'title': track['recording']['title'], + 'album': album, + 'artist': album.artist, + } + ) class TrackFile(models.Model): uuid = models.UUIDField( unique=True, db_index=True, default=uuid.uuid4) diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index 218e374e8239c91861f82dbf75245cd94bee7ead..7b1b4898111f71b6947724dd4119fd1e36e1dfb5 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -259,7 +259,9 @@ def get_cover_from_fs(dir_path): 'import_job') def import_job_run(self, import_job, replace=False, use_acoustid=False): def mark_errored(exc): - logger.error('[Import Job %s] Error during import: %s', str(exc)) + logger.error( + '[Import Job %s] Error during import: %s', + import_job.pk, str(exc)) import_job.status = 'errored' import_job.save(update_fields=['status']) diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index f11e4507a7a0bc7d1648e5dfda18f3deb4542286..3b9fbb21476d8b4e45fa8fc8356d0b0b635e1685 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -43,9 +43,9 @@ def get_query(query_string, search_fields): def guess_mimetype(f): - b = min(100000, f.size) + b = min(1000000, f.size) t = magic.from_buffer(f.read(b), mime=True) - if t == 'application/octet-stream': + if not t.startswith('audio/'): # failure, we try guessing by extension mt, _ = mimetypes.guess_type(f.path) if mt: diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 2f5b75a97a51de248fc128835c50d1b3b926ffd0..2850c077051a7a25b210f3d1eebe728b981bdba4 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -35,7 +35,6 @@ from funkwhale_api.musicbrainz import api from funkwhale_api.requests.models import ImportRequest from . import filters -from . import forms from . import importers from . import models from . import permissions as music_permissions @@ -324,42 +323,6 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): except models.TrackFile.DoesNotExist: return Response(status=404) - @list_route(methods=['get']) - def viewable(self, request, *args, **kwargs): - return Response({}, status=200) - - @list_route(methods=['get']) - def transcode(self, request, *args, **kwargs): - form = forms.TranscodeForm(request.GET) - if not form.is_valid(): - return Response(form.errors, status=400) - - f = form.cleaned_data['track_file'] - if not f.audio_file: - return Response(status=400) - output_kwargs = { - 'format': form.cleaned_data['to'] - } - args = (ffmpeg - .input(f.audio_file.path) - .output('pipe:', **output_kwargs) - .get_args() - ) - # we use a generator here so the view return immediatly and send - # file chunk to the browser, instead of blocking a few seconds - def _transcode(): - p = subprocess.Popen( - ['ffmpeg'] + args, - stdout=subprocess.PIPE) - for line in p.stdout: - yield line - - response = StreamingHttpResponse( - _transcode(), status=200, - content_type=form.cleaned_data['to']) - - return response - class TagViewSet(viewsets.ReadOnlyModelViewSet): queryset = Tag.objects.all().order_by('name') diff --git a/api/funkwhale_api/providers/audiofile/tasks.py b/api/funkwhale_api/providers/audiofile/tasks.py index 40114c8774abfa2f18e520dd15b19be9f382cb58..fb630673555bbb0cff380a5b835d725506a287da 100644 --- a/api/funkwhale_api/providers/audiofile/tasks.py +++ b/api/funkwhale_api/providers/audiofile/tasks.py @@ -12,25 +12,43 @@ from funkwhale_api.music import models, metadata @transaction.atomic def import_track_data_from_path(path): data = metadata.Metadata(path) - artist = models.Artist.objects.get_or_create( - name__iexact=data.get('artist'), - defaults={ - 'name': data.get('artist'), - 'mbid': data.get('musicbrainz_artistid', None), - }, - )[0] + album = None + track_mbid = data.get('musicbrainz_recordingid', None) + album_mbid = data.get('musicbrainz_albumid', None) - 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] + if album_mbid and track_mbid: + # to gain performance and avoid additional mb lookups, + # we import from the release data, which is already cached + return models.Track.get_or_create_from_release( + album_mbid, track_mbid)[0] + elif track_mbid: + return models.Track.get_or_create_from_api(track_mbid)[0] + elif album_mbid: + album = models.Album.get_or_create_from_api(album_mbid)[0] + artist = album.artist if album else None + artist_mbid = data.get('musicbrainz_artistid', None) + if not artist: + if artist_mbid: + artist = models.Artist.get_or_create_from_api(artist_mbid)[0] + else: + artist = models.Artist.objects.get_or_create( + name__iexact=data.get('artist'), + defaults={ + 'name': data.get('artist'), + }, + )[0] + + release_date = data.get('date', default=None) + if not album: + album = models.Album.objects.get_or_create( + title__iexact=data.get('album'), + artist=artist, + defaults={ + 'title': data.get('album'), + 'release_date': release_date, + }, + )[0] position = data.get('track_number', default=None) track = models.Track.objects.get_or_create( title__iexact=data.get('title'), @@ -38,7 +56,6 @@ def import_track_data_from_path(path): defaults={ 'title': data.get('title'), 'position': position, - 'mbid': data.get('musicbrainz_recordingid', None), }, )[0] return track diff --git a/api/tests/music/test_commands.py b/api/tests/music/test_commands.py index ff3343aa53fd13be34c99316c2da38c188571e93..6f03f6b8ad33e3f6a974ff2da8aa91f053d7ad09 100644 --- a/api/tests/music/test_commands.py +++ b/api/tests/music/test_commands.py @@ -1,5 +1,9 @@ +import os + from funkwhale_api.music.management.commands import fix_track_files +DATA_DIR = os.path.dirname(os.path.abspath(__file__)) + def test_fix_track_files_bitrate_length(factories, mocker): tf1 = factories['music.TrackFile'](bitrate=1, duration=2) @@ -43,3 +47,27 @@ def test_fix_track_files_size(factories, mocker): # updated assert tf2.size == 2 + + +def test_fix_track_files_mimetype(factories, mocker): + name = 'test.mp3' + mp3_path = os.path.join(DATA_DIR, 'test.mp3') + ogg_path = os.path.join(DATA_DIR, 'test.ogg') + tf1 = factories['music.TrackFile']( + audio_file__from_path=mp3_path, + source='file://{}'.format(mp3_path), + mimetype='application/x-empty') + + # this one already has a mimetype set, to it should not be updated + tf2 = factories['music.TrackFile']( + audio_file__from_path=ogg_path, + source='file://{}'.format(ogg_path), + mimetype='audio/something') + c = fix_track_files.Command() + c.fix_mimetypes(dry_run=False) + + tf1.refresh_from_db() + tf2.refresh_from_db() + + assert tf1.mimetype == 'audio/mpeg' + assert tf2.mimetype == 'audio/something' diff --git a/api/tests/music/test_metadata.py b/api/tests/music/test_metadata.py index 326f18324bd4ba027d225f28369e42d40e57ee87..a4f15b3355f9e2299e682675fd1622d16ab19353 100644 --- a/api/tests/music/test_metadata.py +++ b/api/tests/music/test_metadata.py @@ -95,3 +95,17 @@ def test_can_get_metadata_from_flac_file_not_crash_if_empty(): with pytest.raises(metadata.TagNotFound): data.get('test') + + +@pytest.mark.parametrize('field_name', [ + 'musicbrainz_artistid', + 'musicbrainz_albumid', + 'musicbrainz_recordingid', +]) +def test_mbid_clean_keeps_only_first(field_name): + u1 = str(uuid.uuid4()) + u2 = str(uuid.uuid4()) + field = metadata.VALIDATION[field_name] + result = field.to_python('/'.join([u1, u2])) + + assert str(result) == u1 diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py index feb68ea33ad53f146b8e05d7a47e288af2285d21..0ef54eb668014462e57649129115b2719e56e2f8 100644 --- a/api/tests/music/test_models.py +++ b/api/tests/music/test_models.py @@ -43,6 +43,53 @@ def test_import_album_stores_release_group(factories): assert album.artist == artist +def test_import_track_from_release(factories, mocker): + album = factories['music.Album']( + mbid='430347cb-0879-3113-9fde-c75b658c298e') + album_data = { + 'release': { + 'id': album.mbid, + 'title': 'Daydream Nation', + 'status': 'Official', + 'medium-count': 1, + 'medium-list': [ + { + 'position': '1', + 'format': 'CD', + 'track-list': [ + { + 'id': '03baca8b-855a-3c05-8f3d-d3235287d84d', + 'position': '4', + 'number': '4', + 'length': '417973', + 'recording': { + 'id': '2109e376-132b-40ad-b993-2bb6812e19d4', + 'title': 'Teen Age Riot', + 'length': '417973'}, + 'track_or_recording_length': '417973' + } + ], + 'track-count': 1 + } + ], + } + } + mocked_get = mocker.patch( + 'funkwhale_api.musicbrainz.api.releases.get', + return_value=album_data) + track_data = album_data['release']['medium-list'][0]['track-list'][0] + track = models.Track.get_or_create_from_release( + '430347cb-0879-3113-9fde-c75b658c298e', + track_data['recording']['id'], + )[0] + mocked_get.assert_called_once_with( + album.mbid, includes=models.Album.api_includes) + assert track.title == track_data['recording']['title'] + assert track.mbid == track_data['recording']['id'] + assert track.album == album + assert track.artist == album.artist + assert track.position == int(track_data['position']) + def test_import_job_is_bound_to_track_file(factories, mocker): track = factories['music.Track']() job = factories['music.ImportJob'](mbid=track.mbid) diff --git a/api/tests/music/test_utils.py b/api/tests/music/test_utils.py index 12b381a997c59ea85ebf1239024b1d3f288e2cdf..7b967dbbcceac9d6aade23d2dfb97d8f2a7d696b 100644 --- a/api/tests/music/test_utils.py +++ b/api/tests/music/test_utils.py @@ -15,9 +15,13 @@ def test_guess_mimetype_try_using_extension(factories, mocker): assert utils.guess_mimetype(f.audio_file) == 'audio/mpeg' -def test_guess_mimetype_try_using_extension_if_fail(factories, mocker): +@pytest.mark.parametrize('wrong', [ + 'application/octet-stream', + 'application/x-empty', +]) +def test_guess_mimetype_try_using_extension_if_fail(wrong, factories, mocker): mocker.patch( - 'magic.from_buffer', return_value='application/octet-stream') + 'magic.from_buffer', return_value=wrong) f = factories['music.TrackFile'].build( audio_file__filename='test.mp3') diff --git a/api/tests/test_import_audio_file.py b/api/tests/test_import_audio_file.py index de81860754a7da6c667ef05302b08ff7310ee6f8..da3d1959cbb076ccadf9ad5cff81ca2affb6dcb0 100644 --- a/api/tests/test_import_audio_file.py +++ b/api/tests/test_import_audio_file.py @@ -15,16 +15,13 @@ DATA_DIR = os.path.join( ) -def test_can_create_track_from_file_metadata(db, mocker): +def test_can_create_track_from_file_metadata_no_mbid(db, mocker): metadata = { 'artist': ['Test artist'], 'album': ['Test album'], 'title': ['Test track'], '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'], } m1 = mocker.patch('mutagen.File', return_value=metadata) m2 = mocker.patch( @@ -35,13 +32,64 @@ def test_can_create_track_from_file_metadata(db, mocker): os.path.join(DATA_DIR, 'dummy_file.ogg')) assert track.title == metadata['title'][0] - assert track.mbid == uuid.UUID(metadata['musicbrainz_trackid'][0]) + assert track.mbid is None assert track.position == 4 assert track.album.title == metadata['album'][0] - assert track.album.mbid == uuid.UUID(metadata['musicbrainz_albumid'][0]) + assert track.album.mbid is None assert track.album.release_date == datetime.date(2012, 8, 15) assert track.artist.name == metadata['artist'][0] - assert track.artist.mbid == uuid.UUID(metadata['musicbrainz_artistid'][0]) + assert track.artist.mbid is None + + +def test_can_create_track_from_file_metadata_mbid(factories, mocker): + album = factories['music.Album']() + mocker.patch( + 'funkwhale_api.music.models.Album.get_or_create_from_api', + return_value=(album, True), + ) + + album_data = { + 'release': { + 'id': album.mbid, + 'medium-list': [ + { + 'track-list': [ + { + 'id': '03baca8b-855a-3c05-8f3d-d3235287d84d', + 'position': '4', + 'number': '4', + 'recording': { + 'id': '2109e376-132b-40ad-b993-2bb6812e19d4', + 'title': 'Teen Age Riot', + }, + } + ], + 'track-count': 1 + } + ], + } + } + mocker.patch( + 'funkwhale_api.musicbrainz.api.releases.get', + return_value=album_data) + track_data = album_data['release']['medium-list'][0]['track-list'][0] + metadata = { + 'musicbrainz_albumid': [album.mbid], + 'musicbrainz_trackid': [track_data['recording']['id']], + } + m1 = mocker.patch('mutagen.File', return_value=metadata) + m2 = mocker.patch( + 'funkwhale_api.music.metadata.Metadata.get_file_type', + return_value='OggVorbis', + ) + track = tasks.import_track_data_from_path( + os.path.join(DATA_DIR, 'dummy_file.ogg')) + + assert track.title == track_data['recording']['title'] + assert track.mbid == track_data['recording']['id'] + assert track.position == 4 + assert track.album == album + assert track.artist == album.artist def test_management_command_requires_a_valid_username(factories, mocker): diff --git a/changes/template.rst b/changes/template.rst index 24f0e87ebc16c474c7b44841114816ab0f6fcb2f..9ffcdc08e6317223ad2f3cf39aedf85da826ecbb 100644 --- a/changes/template.rst +++ b/changes/template.rst @@ -1,5 +1,6 @@ -Upgrade instructions are available at https://docs.funkwhale.audio/upgrading.html +Upgrade instructions are available at +https://docs.funkwhale.audio/upgrading.html {% for section, _ in sections.items() %} {% if sections[section] %} diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 5314d90175f981d7e6b809d45866a2753d3028be..66851321fb43017af8b893319940562309b5b604 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -1,8 +1,5 @@ # Ensure you update at least the server_name variables to match your own -# transcode cache -proxy_cache_path /tmp/funkwhale-transcode levels=1:2 keys_zone=transcode:10m max_size=1g inactive=7d; - # domain upstream funkwhale-api { # depending on your setup, you may want to udpate this @@ -98,41 +95,6 @@ server { alias /srv/funkwhale/data/music; } - # Transcoding logic and caching - location = /transcode-auth { - include /etc/nginx/funkwhale_proxy.conf; - # needed so we can authenticate transcode requests, but still - # cache the result - internal; - set $query ''; - # ensure we actually pass the jwt to the underlytin auth url - if ($request_uri ~* "[^\?]+\?(.*)$") { - set $query $1; - } - proxy_pass http://funkwhale-api/api/v1/trackfiles/viewable/?$query; - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - } - - location /api/v1/trackfiles/transcode/ { - include /etc/nginx/funkwhale_proxy.conf; - # this block deals with authenticating and caching transcoding - # requests. Caching is heavily recommended as transcoding - # is a CPU intensive process. - auth_request /transcode-auth; - if ($args ~ (.*)jwt=[^&]*(.*)) { - set $cleaned_args $1$2; - } - proxy_cache_key "$scheme$request_method$host$uri$is_args$cleaned_args"; - proxy_cache transcode; - proxy_cache_valid 200 7d; - proxy_ignore_headers "Set-Cookie"; - proxy_hide_header "Set-Cookie"; - add_header X-Cache-Status $upstream_cache_status; - proxy_pass http://funkwhale-api; - } - # end of transcoding logic - location /staticfiles/ { # django static files alias /srv/funkwhale/data/static/; diff --git a/docker/nginx/conf.dev b/docker/nginx/conf.dev index 673edd1a4b4422f94e12cd4886dc5296e8760ed7..2ed1a97d540823ab6021df46db677718885c4529 100644 --- a/docker/nginx/conf.dev +++ b/docker/nginx/conf.dev @@ -26,7 +26,6 @@ http { keepalive_timeout 65; #gzip on; - proxy_cache_path /tmp/funkwhale-transcode levels=1:2 keys_zone=transcode:10m max_size=1g inactive=24h use_temp_path=off; map $http_upgrade $connection_upgrade { default upgrade; @@ -46,38 +45,6 @@ http { internal; alias /music; } - location = /transcode-auth { - # needed so we can authenticate transcode requests, but still - # cache the result - internal; - set $query ''; - # ensure we actually pass the jwt to the underlytin auth url - if ($request_uri ~* "[^\?]+\?(.*)$") { - set $query $1; - } - include /etc/nginx/funkwhale_proxy.conf; - proxy_pass http://api:12081/api/v1/trackfiles/viewable/?$query; - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - } - - location /api/v1/trackfiles/transcode/ { - # this block deals with authenticating and caching transcoding - # requests. Caching is heavily recommended as transcoding - # is a CPU intensive process. - auth_request /transcode-auth; - if ($args ~ (.*)jwt=[^&]*(.*)) { - set $cleaned_args $1$2; - } - include /etc/nginx/funkwhale_proxy.conf; - proxy_cache_key "$scheme$request_method$host$uri$is_args$cleaned_args"; - proxy_cache transcode; - proxy_cache_valid 200 7d; - proxy_ignore_headers "Set-Cookie"; - proxy_hide_header "Set-Cookie"; - add_header X-Cache-Status $upstream_cache_status; - proxy_pass http://api:12081; - } location / { include /etc/nginx/funkwhale_proxy.conf; proxy_pass http://api:12081/; diff --git a/docs/installation/index.rst b/docs/installation/index.rst index ae5794b6cfd41ea2074be012d38748c2801bb06a..0628fe17f5c6eac184427dab7d5d8ddb00a3883f 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -16,10 +16,8 @@ The project relies on the following components and services to work: Hardware requirements --------------------- -Funkwhale is not especially CPU hungry, unless you're relying heavily -on the transcoding feature (which is basic and unoptimized at the moment). - -On a dockerized instance with 2 CPUs and a few active users, the memory footprint is around ~500Mb:: +Funkwhale is not especially CPU hungry. On a dockerized instance with 2 CPUs +and a few active users, the memory footprint is around ~500Mb:: CONTAINER MEM USAGE funkwhale_api_1 202.1 MiB @@ -116,6 +114,13 @@ Then, download our sample virtualhost file and proxy conf: Ensure static assets and proxy pass match your configuration, and check the configuration is valid with ``nginx -t``. If everything is fine, you can restart your nginx server with ``service nginx restart``. +.. warning:: + + If you plan to use to in-place import, ensure the alias value + in the ``_protected/music`` location matches your MUSIC_DIRECTORY_SERVE_PATH + env var. + + Apache2 ^^^^^^^ @@ -125,10 +130,8 @@ Apache2 are not working yet: - Websocket (used for real-time updates on Instance timeline) - - Transcoding of audio files - Those features are not necessary to use your Funkwhale instance, and - transcoding in particular is still in alpha-state anyway. + Those features are not necessary to use your Funkwhale instance. Ensure you have a recent version of apache2 installed on your server. You'll also need the following dependencies:: diff --git a/front/src/App.vue b/front/src/App.vue index a213374284fd072b22513fa63f15b52b56458259..673f8386460ecba32737c129e3421adc06881f04 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -12,10 +12,13 @@ <router-link class="item" to="/about"> <i18next path="About this instance" /> </router-link> - <i18next tag="a" href="https://funkwhale.audio" class="item" target="_blank" path="Official website" /> - <i18next tag="a" href="https://docs.funkwhale.audio" class="item" target="_blank" path="Documentation" /> - <i18next tag="a" href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank" path="Source code" /> - <i18next tag="a" href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank" path="Issue tracker" /> + <a href="https://funkwhale.audio" class="item" target="_blank">{{ $t('Official website') }}</a> + <a href="https://docs.funkwhale.audio" class="item" target="_blank">{{ $t('Documentation') }}</a> + <a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank"> + <template v-if="version">{{ $t('Source code ({% version %})', {version: version}) }}</template> + <template v-else>{{ $t('Source code') }}</template> + </a> + <a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">{{ $t('Issue tracker') }}</a> </div> </div> <div class="ten wide column"> @@ -39,6 +42,9 @@ </template> <script> +import axios from 'axios' +import _ from 'lodash' + import Sidebar from '@/components/Sidebar' import Raven from '@/components/Raven' @@ -51,6 +57,11 @@ export default { Raven, PlaylistModal }, + data () { + return { + nodeinfo: null + } + }, created () { this.$store.dispatch('instance/fetchSettings') let self = this @@ -58,6 +69,23 @@ export default { // used to redraw ago dates every minute self.$store.commit('ui/computeLastDate') }, 1000 * 60) + this.fetchNodeInfo() + }, + methods: { + fetchNodeInfo () { + let self = this + axios.get('instance/nodeinfo/2.0/').then(response => { + self.nodeinfo = response.data + }) + } + }, + computed: { + version () { + if (!this.nodeinfo) { + return null + } + return _.get(this.nodeinfo, 'software.version') + } } } </script> diff --git a/front/src/audio/backend.js b/front/src/audio/backend.js index 5b4c707067698822f23f831284efcdabb82c2dc3..619f3cefdbd7b08f9879be343ce246e41e86de0d 100644 --- a/front/src/audio/backend.js +++ b/front/src/audio/backend.js @@ -26,7 +26,11 @@ export default { return url } if (url.startsWith('/')) { - return config.BACKEND_URL + url.substr(1) + let rootUrl = ( + window.location.protocol + '//' + window.location.hostname + + (window.location.port ? ':' + window.location.port : '') + ) + return rootUrl + url } else { return config.BACKEND_URL + url } diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index 6fc7699690f53ab66e998d320655327f51d21cc9..28a8900841afc29fdefe844b3b8c473420f7809c 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -67,8 +67,31 @@ export default { } }, methods: { + getTracksPage (page, params, resolve, tracks) { + if (page > 10) { + // it's 10 * 100 tracks already, let's stop here + resolve(tracks) + } + // when fetching artists/or album tracks, sometimes, we may have to fetch + // multiple pages + let self = this + params['page_size'] = 100 + params['page'] = page + tracks = tracks || [] + axios.get('tracks/', {params: params}).then((response) => { + response.data.results.forEach(t => { + tracks.push(t) + }) + if (response.data.next) { + self.getTracksPage(page + 1, params, resolve, tracks) + } else { + resolve(tracks) + } + }) + }, getPlayableTracks () { let self = this + this.isLoading = true let getTracks = new Promise((resolve, reject) => { if (self.track) { resolve([self.track]) @@ -82,44 +105,30 @@ export default { })) }) } else if (self.artist) { - let params = { - params: {'artist': self.artist, 'ordering': 'album__release_date,position'} - } - axios.get('tracks', params).then((response) => { - resolve(response.data.results) - }) + let params = {'artist': self.artist, 'ordering': 'album__release_date,position'} + self.getTracksPage(1, params, resolve) } else if (self.album) { - let params = { - params: {'album': self.album, 'ordering': 'position'} - } - axios.get('tracks', params).then((response) => { - resolve(response.data.results) - }) + let params = {'album': self.album, 'ordering': 'position'} + self.getTracksPage(1, params, resolve) } }) return getTracks.then((tracks) => { + setTimeout(e => { + self.isLoading = false + }, 250) return tracks.filter(e => { return e.files.length > 0 }) }) }, - triggerLoad () { - let self = this - this.isLoading = true - setTimeout(() => { - self.isLoading = false - }, 500) - }, add () { let self = this - this.triggerLoad() this.getPlayableTracks().then((tracks) => { self.$store.dispatch('queue/appendMany', {tracks: tracks}) }) }, addNext (next) { let self = this - this.triggerLoad() let wasEmpty = this.$store.state.queue.tracks.length === 0 this.getPlayableTracks().then((tracks) => { self.$store.dispatch('queue/appendMany', {tracks: tracks, index: self.$store.state.queue.currentIndex + 1}) diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue index 890c83f5bfc2d6562eedfda26b9a63a4fe631bf1..9cfea3bc0cae5da768bbf1dbefe7e12b405f508c 100644 --- a/front/src/components/audio/Search.vue +++ b/front/src/components/audio/Search.vue @@ -29,9 +29,9 @@ </template> <script> +import _ from 'lodash' import axios from 'axios' import logger from '@/logging' -import backend from '@/audio/backend' import AlbumCard from '@/components/audio/album/Card' import ArtistCard from '@/components/audio/artist/Card' @@ -50,7 +50,6 @@ export default { albums: [], artists: [] }, - backend: backend, isLoading: false } }, @@ -61,7 +60,7 @@ export default { this.search() }, methods: { - search () { + search: _.debounce(function () { if (this.query.length < 1) { return } @@ -77,15 +76,11 @@ export default { self.results = self.castResults(response.data) self.isLoading = false }) - }, + }, 500), castResults (results) { return { - albums: results.albums.map((album) => { - return backend.Album.clean(album) - }), - artists: results.artists.map((artist) => { - return backend.Artist.clean(artist) - }) + albums: results.albums, + artists: results.artists } } }, diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue index 08a055f5ca97186d51130e419176338473f9390e..366f104f1fc021fbe5478346439e94034b345e2a 100644 --- a/front/src/components/audio/Track.vue +++ b/front/src/components/audio/Track.vue @@ -18,7 +18,6 @@ <script> import {mapState} from 'vuex' import url from '@/utils/url' -import formats from '@/audio/formats' import _ from 'lodash' // import logger from '@/logging' @@ -52,13 +51,6 @@ export default { let sources = [ {type: file.mimetype, url: file.path} ] - formats.formats.forEach(f => { - if (f !== file.mimetype) { - let format = formats.formatsMap[f] - let url = `/api/v1/trackfiles/transcode/?track_file=${file.id}&to=${format}` - sources.push({type: f, url: url}) - } - }) if (this.$store.state.auth.authenticated) { // we need to send the token directly in url // so authentication can be checked by the backend diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue index 7045cf9bd222085bbb7fbf1056a24f4c8205cb86..4559b3c41b59ccf798eec071b97c1ab55a81dceb 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -84,4 +84,7 @@ export default { tr:not(:hover) .favorite-icon:not(.favorited) { display: none; } +pre { + overflow-x: scroll; +} </style> diff --git a/front/src/components/library/import/FileUpload.vue b/front/src/components/library/import/FileUpload.vue index 7aa8adac0f00f72c9724603095977b6797aa4ea9..48ca0ad84adf41744303ed363d070e34daaed2bd 100644 --- a/front/src/components/library/import/FileUpload.vue +++ b/front/src/components/library/import/FileUpload.vue @@ -9,7 +9,6 @@ :class="['ui', 'icon', 'left', 'floated', 'button']" :post-action="uploadUrl" :multiple="true" - :size="1024 * 1024 * 30" :data="uploadData" :drop="true" extensions="ogg,mp3,flac"