diff --git a/.env.dev b/.env.dev index bc2d667b1..d42cdad02 100644 --- a/.env.dev +++ b/.env.dev @@ -1,3 +1,4 @@ -BACKEND_URL=http://localhost:6001 API_AUTHENTICATION_REQUIRED=True CACHALOT_ENABLED=False +RAVEN_ENABLED=false +RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91b11e8bd..0fa450c46 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ variables: IMAGE_NAME: funkwhale/funkwhale IMAGE: $IMAGE_NAME:$CI_COMMIT_REF_NAME IMAGE_LATEST: $IMAGE_NAME:latest - + PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache" stages: @@ -14,40 +14,62 @@ test_api: services: - postgres:9.4 stage: test - image: funkwhale/funkwhale:base + image: funkwhale/funkwhale:latest + cache: + key: "$CI_PROJECT_ID/pip_cache" + paths: + - "$PIP_CACHE_DIR" variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache" + DJANGO_ALLOWED_HOSTS: "localhost" DATABASE_URL: "postgresql://postgres@postgres/postgres" before_script: - - python3 -m venv --copies virtualenv - - source virtualenv/bin/activate - cd api - pip install -r requirements/base.txt - pip install -r requirements/local.txt - pip install -r requirements/test.txt script: - pytest - cache: - key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" - paths: - - "$CI_PROJECT_DIR/pip-cache" tags: - docker -build_front: - stage: build - image: node:6-alpine + +test_front: + stage: test + image: node:9 before_script: - cd front script: - - npm install - - npm run build + - yarn install + - yarn run unit cache: - key: "$CI_COMMIT_REF_NAME" + key: "$CI_PROJECT_ID/front_dependencies" paths: - front/node_modules + - front/yarn.lock + artifacts: + name: "front_${CI_COMMIT_REF_NAME}" + paths: + - front/dist/ + tags: + - docker + + +build_front: + stage: build + image: node:9 + before_script: + - cd front + + script: + - yarn install + - yarn run build + cache: + key: "$CI_PROJECT_ID/front_dependencies" + paths: + - front/node_modules + - front/yarn.lock artifacts: name: "front_${CI_COMMIT_REF_NAME}" paths: diff --git a/CHANGELOG b/CHANGELOG index 6909d7d78..2d005e1a3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,8 +2,23 @@ Changelog ========= -0.3.5 (Unreleased) ------------------- +0.5 (Unreleased) +---------------- + + +0.4 (2018-02-18) +---------------- + +- Front: ambiant colors in player based on current track cover (#59) +- Front: simplified front dev setup thanks to webpack proxy (#59) +- Front: added some unittests for the store (#55) +- Front: fixed broken login redirection when 401 +- Front: Removed autoplay on page reload +- API: Added a /instance/settings endpoint +- Front: load /instance/settings on page load +- Added settings to report JS and Python error to a Sentry instance + This is disabled by default, but feel free to enable it if you want + to help us by sending your error reports :) (#8) 0.3.5 (2018-01-07) diff --git a/api/config/api_urls.py b/api/config/api_urls.py index d64eeb5fd..c7ebc4ed3 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -1,5 +1,6 @@ from rest_framework import routers from django.conf.urls import include, url +from funkwhale_api.instance import views as instance_views from funkwhale_api.music import views from funkwhale_api.playlists import views as playlists_views from rest_framework_jwt import views as jwt_views @@ -25,6 +26,10 @@ router.register( v1_patterns = router.urls v1_patterns += [ + url(r'^instance/', + include( + ('funkwhale_api.instance.urls', 'instance'), + namespace='instance')), url(r'^providers/', include( ('funkwhale_api.providers.urls', 'providers'), diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 6f821dfba..6d02cbbc1 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -12,6 +12,7 @@ from __future__ import absolute_import, unicode_literals import os import environ +from funkwhale_api import __version__ ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /) APPS_DIR = ROOT_DIR.path('funkwhale_api') @@ -22,6 +23,10 @@ try: env.read_env(ROOT_DIR.file('.env')) except FileNotFoundError: pass + +ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS') + + # APP CONFIGURATION # ------------------------------------------------------------------------------ DJANGO_APPS = ( @@ -56,10 +61,28 @@ THIRD_PARTY_APPS = ( 'django_filters', ) + +# Sentry +RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False) +RAVEN_DSN = env("RAVEN_DSN", default='') + +if RAVEN_ENABLED: + RAVEN_CONFIG = { + 'dsn': RAVEN_DSN, + # If you are using git, you can also automatically configure the + # release based on the git info. + 'release': __version__, + } + THIRD_PARTY_APPS += ( + 'raven.contrib.django.raven_compat', + ) + + # Apps specific for this project go here. LOCAL_APPS = ( 'funkwhale_api.users', # custom users app # Your stuff: custom apps go here + 'funkwhale_api.instance', 'funkwhale_api.music', 'funkwhale_api.favorites', 'funkwhale_api.radios', @@ -71,6 +94,7 @@ LOCAL_APPS = ( ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps + INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS # MIDDLEWARE CONFIGURATION diff --git a/api/config/settings/production.py b/api/config/settings/production.py index e00983305..df15d325f 100644 --- a/api/config/settings/production.py +++ b/api/config/settings/production.py @@ -54,7 +54,6 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # ------------------------------------------------------------------------------ # Hosts/domain names that are valid for this site # See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts -ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS') CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS # END SITE CONFIGURATION diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py index 11607c4e1..d1c7fcdf4 100644 --- a/api/funkwhale_api/__init__.py +++ b/api/funkwhale_api/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = '0.3.5' +__version__ = '0.4' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index 98c0cfc08..08ae00b68 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -43,7 +43,7 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin, favorite = models.TrackFavorite.add(track=track, user=self.request.user) return favorite - @list_route(methods=['delete']) + @list_route(methods=['delete', 'post']) def remove(self, request, *args, **kwargs): try: pk = int(request.data['track']) diff --git a/api/funkwhale_api/instance/__init__.py b/api/funkwhale_api/instance/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py new file mode 100644 index 000000000..1d93c383e --- /dev/null +++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py @@ -0,0 +1,37 @@ +from dynamic_preferences import types +from dynamic_preferences.registries import global_preferences_registry + +raven = types.Section('raven') + + +@global_preferences_registry.register +class RavenDSN(types.StringPreference): + show_in_api = True + section = raven + name = 'front_dsn' + default = 'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4' + verbose_name = ( + 'A raven DSN key used to report front-ent errors to ' + 'a sentry instance' + ) + help_text = ( + 'Keeping the default one will report errors to funkwhale developers' + ) + + +SENTRY_HELP_TEXT = ( + 'Error reporting is disabled by default but you can enable it if' + ' you want to help us improve funkwhale' +) + + +@global_preferences_registry.register +class RavenEnabled(types.BooleanPreference): + show_in_api = True + section = raven + name = 'front_enabled' + default = False + verbose_name = ( + 'Wether error reporting to a Sentry instance using raven is enabled' + ' for front-end errors' + ) diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py new file mode 100644 index 000000000..2f2b46b87 --- /dev/null +++ b/api/funkwhale_api/instance/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from . import views + + +urlpatterns = [ + url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'), +] diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py new file mode 100644 index 000000000..44ee22873 --- /dev/null +++ b/api/funkwhale_api/instance/views.py @@ -0,0 +1,25 @@ +from rest_framework import views +from rest_framework.response import Response + +from dynamic_preferences.api import serializers +from dynamic_preferences.registries import global_preferences_registry + + +class InstanceSettings(views.APIView): + permission_classes = [] + authentication_classes = [] + + def get(self, request, *args, **kwargs): + manager = global_preferences_registry.manager() + manager.all() + all_preferences = manager.model.objects.all().order_by( + 'section', 'name' + ) + api_preferences = [ + p + for p in all_preferences + if getattr(p.preference, 'show_in_api', False) + ] + data = serializers.GlobalPreferenceSerializer( + api_preferences, many=True).data + return Response(data, status=200) diff --git a/api/funkwhale_api/music/fake_data.py b/api/funkwhale_api/music/fake_data.py index ef5eaf4be..892b784ca 100644 --- a/api/funkwhale_api/music/fake_data.py +++ b/api/funkwhale_api/music/fake_data.py @@ -4,7 +4,7 @@ Populates the database with fake data import random from funkwhale_api.music import models -from funkwhale_api.music.tests import factories +from funkwhale_api.music import factories def create_data(count=25): @@ -19,4 +19,4 @@ def create_data(count=25): if __name__ == '__main__': - main() + create_data() diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 9f96dad0e..506893a4d 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -31,10 +31,7 @@ class TrackFileSerializer(serializers.ModelSerializer): fields = ('id', 'path', 'duration', 'source', 'filename', 'track') def get_path(self, o): - request = self.context.get('request') url = o.path - if request: - url = request.build_absolute_uri(url) return url diff --git a/api/requirements/base.txt b/api/requirements/base.txt index cff16d3f1..f38da9629 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -47,8 +47,7 @@ mutagen>=1.39,<1.40 # Until this is merged -#django-taggit>=0.22,<0.23 -git+https://github.com/alex/django-taggit.git@95776ac66948ed7ba7c12e35c1170551e3be66a5 +django-taggit>=0.22,<0.23 # Until this is merged git+https://github.com/EliotBerriot/PyMemoize.git@django # Until this is merged @@ -57,3 +56,4 @@ git+https://github.com/EliotBerriot/django-cachalot.git@django-2 django-dynamic-preferences>=1.5,<1.6 pyacoustid>=1.1.5,<1.2 +raven>=6.5,<7 diff --git a/api/test.yml b/api/test.yml index c59ce45bb..e892dfb17 100644 --- a/api/test.yml +++ b/api/test.yml @@ -10,6 +10,7 @@ services: volumes: - .:/app environment: + - "DJANGO_ALLOWED_HOSTS=localhost" - "DATABASE_URL=postgresql://postgres@postgres/postgres" postgres: image: postgres diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 6c0cffa4e..4d7a6fa98 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -3,6 +3,7 @@ import shutil import pytest from django.core.cache import cache as django_cache from dynamic_preferences.registries import global_preferences_registry +from rest_framework.test import APIClient from funkwhale_api.taskapp import celery @@ -29,7 +30,9 @@ def factories(db): @pytest.fixture def preferences(db): - yield global_preferences_registry.manager() + manager = global_preferences_registry.manager() + manager.all() + yield manager @pytest.fixture @@ -48,6 +51,11 @@ def logged_in_client(db, factories, client): delattr(client, 'user') +@pytest.fixture +def api_client(client): + return APIClient() + + @pytest.fixture def superuser_client(db, factories, client): user = factories['users.SuperUser']() diff --git a/api/tests/instance/test_preferences.py b/api/tests/instance/test_preferences.py new file mode 100644 index 000000000..c89bfa349 --- /dev/null +++ b/api/tests/instance/test_preferences.py @@ -0,0 +1,22 @@ +from django.urls import reverse + +from dynamic_preferences.api import serializers + + +def test_can_list_settings_via_api(preferences, api_client): + url = reverse('api:v1:instance:settings') + all_preferences = preferences.model.objects.all() + expected_preferences = { + p.preference.identifier(): p + for p in all_preferences + if getattr(p.preference, 'show_in_api', False)} + + assert len(expected_preferences) > 0 + + response = api_client.get(url) + assert response.status_code == 200 + assert len(response.data) == len(expected_preferences) + + for p in response.data: + i = '__'.join([p['section'], p['name']]) + assert i in expected_preferences diff --git a/api/tests/test_favorites.py b/api/tests/test_favorites.py index 418166d8e..8165722ea 100644 --- a/api/tests/test_favorites.py +++ b/api/tests/test_favorites.py @@ -58,11 +58,14 @@ def test_user_can_remove_favorite_via_api(logged_in_client, factories, client): assert response.status_code == 204 assert TrackFavorite.objects.count() == 0 -def test_user_can_remove_favorite_via_api_using_track_id(factories, logged_in_client): + +@pytest.mark.parametrize('method', ['delete', 'post']) +def test_user_can_remove_favorite_via_api_using_track_id( + method, factories, logged_in_client): favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user) url = reverse('api:v1:favorites:tracks-remove') - response = logged_in_client.delete( + response = getattr(logged_in_client, method)( url, json.dumps({'track': favorite.track.pk}), content_type='application/json' ) diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index 9cbe278e8..5bdfeb9c6 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -78,3 +78,10 @@ API_AUTHENTICATION_REQUIRED=True # public: anybody can register an account # disabled: nobody can register an account REGISTRATION_MODE=disabled + +# Sentry/Raven error reporting (server side) +# Enable Raven if you want to help improve funkwhale by +# automatically sending error reports our Sentry instance. +# This will help us detect and correct bugs +RAVEN_ENABLED=false +RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5 diff --git a/dev.yml b/dev.yml index befc4b243..e3cd50da7 100644 --- a/dev.yml +++ b/dev.yml @@ -3,9 +3,7 @@ version: '2' services: front: - build: - dockerfile: docker/Dockerfile.dev - context: ./front + build: front env_file: .env.dev environment: - "HOST=0.0.0.0" @@ -51,13 +49,11 @@ services: - ./api:/app - ./data/music:/music environment: - - "DJANGO_ALLOWED_HOSTS=localhost" + - "DJANGO_ALLOWED_HOSTS=localhost,nginx" - "DJANGO_SETTINGS_MODULE=config.settings.local" - "DJANGO_SECRET_KEY=dev" - "DATABASE_URL=postgresql://postgres@postgres/postgres" - "CACHE_URL=redis://redis:6379/0" - ports: - - "12081:12081" links: - postgres - redis diff --git a/docker/nginx/conf.dev b/docker/nginx/conf.dev index 48436173b..1b749c30a 100644 --- a/docker/nginx/conf.dev +++ b/docker/nginx/conf.dev @@ -40,8 +40,8 @@ http { 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 $scheme; - proxy_set_header X-Forwarded-Host $host:$server_port; - proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Host localhost:8080; + proxy_set_header X-Forwarded-Port 8080; proxy_redirect off; proxy_pass http://api:12081/; } diff --git a/front/Dockerfile b/front/Dockerfile index ad05f72eb..cdf92446b 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,13 +1,11 @@ -FROM node:6-alpine +FROM node:9 EXPOSE 8080 - -RUN mkdir /app -WORKDIR /app +WORKDIR /app/ ADD package.json . +RUN yarn install --only=production +RUN yarn install --only=dev +VOLUME ["/app/node_modules"] +COPY . . -RUN npm install - -ADD . . - -RUN npm run build +CMD ["npm", "run", "dev"] diff --git a/front/config/index.js b/front/config/index.js index a312c7b26..7ce6e26e1 100644 --- a/front/config/index.js +++ b/front/config/index.js @@ -28,7 +28,20 @@ module.exports = { autoOpenBrowser: true, assetsSubDirectory: 'static', assetsPublicPath: '/', - proxyTable: {}, + proxyTable: { + '/api': { + target: 'http://nginx:6001', + changeOrigin: true, + }, + '/media': { + target: 'http://nginx:6001', + changeOrigin: true, + }, + '/staticfiles': { + target: 'http://nginx:6001', + changeOrigin: true, + } + }, // CSS Sourcemaps off by default because relative paths are "buggy" // with this option, according to the CSS-Loader README // (https://github.com/webpack/css-loader#sourcemaps) diff --git a/front/config/prod.env.js b/front/config/prod.env.js index fe0e80b8f..decfe3615 100644 --- a/front/config/prod.env.js +++ b/front/config/prod.env.js @@ -1,4 +1,4 @@ module.exports = { NODE_ENV: '"production"', - BACKEND_URL: '"' + (process.env.BACKEND_URL || '/') + '"' + BACKEND_URL: '"/"' } diff --git a/front/docker/Dockerfile.dev b/front/docker/Dockerfile.dev deleted file mode 100644 index 1a0c90c9e..000000000 --- a/front/docker/Dockerfile.dev +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:6-alpine - -EXPOSE 8080 - -RUN mkdir /app -WORKDIR /app -ADD package.json . - -RUN npm install - -VOLUME ["/app/node_modules"] - -CMD ["npm", "run", "dev"] diff --git a/front/package.json b/front/package.json index 58c22a408..ac3895f6d 100644 --- a/front/package.json +++ b/front/package.json @@ -9,24 +9,28 @@ "start": "node build/dev-server.js", "build": "node build/build.js", "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", + "unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { + "axios": "^0.17.1", "dateformat": "^2.0.0", "js-logger": "^1.3.0", "jwt-decode": "^2.2.0", "lodash": "^4.17.4", + "moxios": "^0.4.0", + "raven-js": "^3.22.3", "semantic-ui-css": "^2.2.10", "vue": "^2.3.3", "vue-lazyload": "^1.1.4", - "vue-resource": "^1.3.4", "vue-router": "^2.3.1", "vue-upload-component": "^2.7.4", "vuedraggable": "^2.14.1", "vuex": "^3.0.1", - "vuex-persistedstate": "^2.4.2" + "vuex-persistedstate": "^2.4.2", + "vuex-router-sync": "^5.0.0" }, "devDependencies": { "autoprefixer": "^6.7.2", @@ -46,6 +50,7 @@ "cross-env": "^4.0.0", "cross-spawn": "^5.0.1", "css-loader": "^0.28.0", + "es6-promise": "^4.2.2", "eslint": "^3.19.0", "eslint-config-standard": "^6.2.1", "eslint-friendly-formatter": "^2.0.7", @@ -67,6 +72,7 @@ "karma-phantomjs-launcher": "^1.0.2", "karma-phantomjs-shim": "^1.4.0", "karma-sinon-chai": "^1.3.1", + "karma-sinon-stub-promise": "^1.0.0", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "0.0.30", "karma-webpack": "^2.0.2", @@ -85,6 +91,7 @@ "shelljs": "^0.7.6", "sinon": "^2.1.0", "sinon-chai": "^2.8.0", + "sinon-stub-promise": "^4.0.0", "url-loader": "^0.5.8", "vue-loader": "^12.1.0", "vue-style-loader": "^3.0.1", diff --git a/front/src/App.vue b/front/src/App.vue index d1d63e651..98ad48d3f 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -22,15 +22,26 @@ + + @@ -40,25 +51,33 @@ export default { // and we end up with CSS rules not applied, // see https://github.com/webpack/webpack/issues/215 @import 'semantic/semantic.css'; +@import 'style/vendor/media'; +html, body { + @include media("desktop") { + margin-left: 350px !important; + } transform: none !important; } .main-pusher { padding: 1.5rem 0; } -#footer { - padding: 4em; -} -.ui.stripe.segment { - padding: 4em; +.ui.stripe.segment, #footer { + padding: 2em; + @include media(">tablet") { + padding: 4em; + } } .ui.small.text.container { diff --git a/front/src/assets/logo/favicon.ico b/front/src/assets/logo/favicon.png similarity index 100% rename from front/src/assets/logo/favicon.ico rename to front/src/assets/logo/favicon.png diff --git a/front/src/components/Raven.vue b/front/src/components/Raven.vue new file mode 100644 index 000000000..e5e125b81 --- /dev/null +++ b/front/src/components/Raven.vue @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index a315aab19..86ec57819 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -1,13 +1,16 @@ - + - + - - + + @@ -49,7 +52,7 @@ - + {{ index + 1}} @@ -84,9 +87,7 @@ - - - + @@ -111,7 +112,8 @@ export default { }, data () { return { - backend: backend + backend: backend, + isCollapsed: true } }, mounted () { @@ -119,7 +121,8 @@ export default { }, computed: { ...mapState({ - queue: state => state.queue + queue: state => state.queue, + url: state => state.route.path }) }, methods: { @@ -129,19 +132,42 @@ export default { reorder: function (oldValue, newValue) { this.$store.commit('queue/reorder', {oldValue, newValue}) } + }, + watch: { + url: function () { + this.isCollapsed = true + } } } diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index 500f4dc1d..e44a92d4f 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -1,145 +1,147 @@ - - - + + + + - - - - - - - - - {{ currentTrack.title }} - - - - {{ currentTrack.artist.name }} - / - - {{ currentTrack.album.title }} + + + + + + + + + {{ currentTrack.title }} - - - + + + {{ currentTrack.artist.name }} + / + + {{ currentTrack.album.title }} + + + + + - - - - - {{currentTimeFormatted}} - + + + + {{currentTimeFormatted}} + - - {{durationFormatted}} + + {{durationFormatted}} + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + - -
{{currentTimeFormatted}}
{{durationFormatted}}