From 979c554b4af8bd68800a6a9cb1b93dfc54385116 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Wed, 18 Jul 2018 15:37:07 +0200
Subject: [PATCH] Use cropped covers/avatars to reduce bandwidth use

---
 api/config/settings/common.py                 | 10 +++++++
 .../common/management/commands/script.py      |  2 +-
 api/funkwhale_api/common/scripts/__init__.py  |  6 ++++
 .../common/scripts/create_image_variations.py | 30 +++++++++++++++++++
 api/funkwhale_api/favorites/views.py          |  6 +++-
 api/funkwhale_api/music/models.py             | 12 ++++++++
 api/funkwhale_api/music/serializers.py        |  9 ++++++
 api/funkwhale_api/playlists/serializers.py    |  2 +-
 api/funkwhale_api/users/models.py             | 12 ++++++++
 api/funkwhale_api/users/serializers.py        | 10 +------
 api/tests/music/test_serializers.py           | 14 +++++++--
 api/tests/playlists/test_serializers.py       | 10 +++----
 changes/changelog.d/image.enhancement         | 25 ++++++++++++++++
 front/src/components/Sidebar.vue              |  2 +-
 front/src/components/audio/Player.vue         |  2 +-
 front/src/components/audio/album/Card.vue     |  2 +-
 front/src/components/audio/album/Widget.vue   | 10 +++----
 front/src/components/audio/artist/Card.vue    |  2 +-
 front/src/components/audio/track/Row.vue      |  2 +-
 front/src/components/audio/track/Widget.vue   | 11 +++++--
 front/src/components/library/Album.vue        |  6 ++--
 front/src/components/library/Artist.vue       |  4 +--
 front/src/components/playlists/Widget.vue     | 11 +++++--
 23 files changed, 161 insertions(+), 39 deletions(-)
 create mode 100644 api/funkwhale_api/common/scripts/create_image_variations.py
 create mode 100644 changes/changelog.d/image.enhancement

diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index c789c36a..b860b1c3 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -467,3 +467,13 @@ MUSIC_DIRECTORY_SERVE_PATH = env(
 USERS_INVITATION_EXPIRATION_DAYS = env.int(
     "USERS_INVITATION_EXPIRATION_DAYS", default=14
 )
+
+VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
+    "square": [
+        ("original", "url"),
+        ("square_crop", "crop__400x400"),
+        ("medium_square_crop", "crop__200x200"),
+        ("small_square_crop", "crop__50x50"),
+    ]
+}
+VERSATILEIMAGEFIELD_SETTINGS = {"create_images_on_demand": False}
diff --git a/api/funkwhale_api/common/management/commands/script.py b/api/funkwhale_api/common/management/commands/script.py
index cbfc78f0..7f8d5c15 100644
--- a/api/funkwhale_api/common/management/commands/script.py
+++ b/api/funkwhale_api/common/management/commands/script.py
@@ -19,7 +19,7 @@ class Command(BaseCommand):
     def handle(self, *args, **options):
         name = options["script_name"]
         if not name:
-            self.show_help()
+            return self.show_help()
 
         available_scripts = self.get_scripts()
         try:
diff --git a/api/funkwhale_api/common/scripts/__init__.py b/api/funkwhale_api/common/scripts/__init__.py
index e69de29b..863256ba 100644
--- a/api/funkwhale_api/common/scripts/__init__.py
+++ b/api/funkwhale_api/common/scripts/__init__.py
@@ -0,0 +1,6 @@
+from . import create_image_variations
+from . import django_permissions_to_user_permissions
+from . import test
+
+
+__all__ = ["create_image_variations", "django_permissions_to_user_permissions", "test"]
diff --git a/api/funkwhale_api/common/scripts/create_image_variations.py b/api/funkwhale_api/common/scripts/create_image_variations.py
new file mode 100644
index 00000000..5e941ce1
--- /dev/null
+++ b/api/funkwhale_api/common/scripts/create_image_variations.py
@@ -0,0 +1,30 @@
+"""
+Compute different sizes of image used for Album covers and User avatars
+"""
+
+from versatileimagefield.image_warmer import VersatileImageFieldWarmer
+
+from funkwhale_api.music.models import Album
+from funkwhale_api.users.models import User
+
+
+MODELS = [(Album, "cover", "square"), (User, "avatar", "square")]
+
+
+def main(command, **kwargs):
+    for model, attribute, key_set in MODELS:
+        qs = model.objects.exclude(**{"{}__isnull".format(attribute): True})
+        qs = qs.exclude(**{attribute: ""})
+        warmer = VersatileImageFieldWarmer(
+            instance_or_queryset=qs,
+            rendition_key_set=key_set,
+            image_attr=attribute,
+            verbose=True,
+        )
+        command.stdout.write(
+            "Creating images for {} / {}".format(model.__name__, attribute)
+        )
+        num_created, failed_to_create = warmer.warm()
+        command.stdout.write(
+            "  {} created, {} in error".format(num_created, len(failed_to_create))
+        )
diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py
index 61b5bee6..ae47e03f 100644
--- a/api/funkwhale_api/favorites/views.py
+++ b/api/funkwhale_api/favorites/views.py
@@ -18,7 +18,11 @@ class TrackFavoriteViewSet(
 ):
 
     serializer_class = serializers.UserTrackFavoriteSerializer
-    queryset = models.TrackFavorite.objects.all()
+    queryset = (
+        models.TrackFavorite.objects.all()
+        .select_related("track__artist", "track__album__artist", "user")
+        .prefetch_related("track__files")
+    )
     permission_classes = [
         permissions.ConditionalAuthentication,
         permissions.OwnerPermission,
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 207b22df..c8dd6131 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -15,7 +15,9 @@ from django.dispatch import receiver
 from django.urls import reverse
 from django.utils import timezone
 from taggit.managers import TaggableManager
+
 from versatileimagefield.fields import VersatileImageField
+from versatileimagefield.image_warmer import VersatileImageFieldWarmer
 
 from funkwhale_api import downloader, musicbrainz
 from funkwhale_api.federation import utils as federation_utils
@@ -641,3 +643,13 @@ def update_request_status(sender, instance, created, **kwargs):
         # let's mark the request as imported since the import is over
         instance.import_request.status = "imported"
         return instance.import_request.save(update_fields=["status"])
+
+
+@receiver(models.signals.post_save, sender=Album)
+def warm_album_covers(sender, instance, **kwargs):
+    if not instance.cover:
+        return
+    album_covers_warmer = VersatileImageFieldWarmer(
+        instance_or_queryset=instance, rendition_key_set="square", image_attr="cover"
+    )
+    num_created, failed_to_create = album_covers_warmer.warm()
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 14ea54d5..0661eb8f 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -1,6 +1,7 @@
 from django.db.models import Q
 from rest_framework import serializers
 from taggit.models import Tag
+from versatileimagefield.serializers import VersatileImageFieldSerializer
 
 from funkwhale_api.activity import serializers as activity_serializers
 from funkwhale_api.users.serializers import UserBasicSerializer
@@ -8,8 +9,12 @@ from funkwhale_api.users.serializers import UserBasicSerializer
 from . import models, tasks
 
 
+cover_field = VersatileImageFieldSerializer(allow_null=True, sizes="square")
+
+
 class ArtistAlbumSerializer(serializers.ModelSerializer):
     tracks_count = serializers.SerializerMethodField()
+    cover = cover_field
 
     class Meta:
         model = models.Album
@@ -87,6 +92,7 @@ class AlbumTrackSerializer(serializers.ModelSerializer):
 class AlbumSerializer(serializers.ModelSerializer):
     tracks = serializers.SerializerMethodField()
     artist = ArtistSimpleSerializer(read_only=True)
+    cover = cover_field
 
     class Meta:
         model = models.Album
@@ -111,6 +117,7 @@ class AlbumSerializer(serializers.ModelSerializer):
 
 class TrackAlbumSerializer(serializers.ModelSerializer):
     artist = ArtistSimpleSerializer(read_only=True)
+    cover = cover_field
 
     class Meta:
         model = models.Album
@@ -156,6 +163,8 @@ class TagSerializer(serializers.ModelSerializer):
 
 
 class SimpleAlbumSerializer(serializers.ModelSerializer):
+    cover = cover_field
+
     class Meta:
         model = models.Album
         fields = ("id", "mbid", "title", "release_date", "cover")
diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py
index 71b8f315..a60a3493 100644
--- a/api/funkwhale_api/playlists/serializers.py
+++ b/api/funkwhale_api/playlists/serializers.py
@@ -107,7 +107,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
         covers = []
         max_covers = 5
         for plt in plts:
-            url = plt.track.album.cover.url
+            url = plt.track.album.cover.crop["200x200"].url
             if url in covers:
                 continue
             covers.append(url)
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index a56406d8..6cef3900 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -11,12 +11,14 @@ import uuid
 from django.conf import settings
 from django.contrib.auth.models import AbstractUser
 from django.db import models
+from django.dispatch import receiver
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
 
 from versatileimagefield.fields import VersatileImageField
+from versatileimagefield.image_warmer import VersatileImageFieldWarmer
 
 from funkwhale_api.common import fields, preferences
 from funkwhale_api.common import utils as common_utils
@@ -205,3 +207,13 @@ class Invitation(models.Model):
             )
 
         return super().save(**kwargs)
+
+
+@receiver(models.signals.post_save, sender=User)
+def warm_user_avatar(sender, instance, **kwargs):
+    if not instance.avatar:
+        return
+    user_avatar_warmer = VersatileImageFieldWarmer(
+        instance_or_queryset=instance, rendition_key_set="square", image_attr="avatar"
+    )
+    num_created, failed_to_create = user_avatar_warmer.warm()
diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py
index a13a44c8..74b06022 100644
--- a/api/funkwhale_api/users/serializers.py
+++ b/api/funkwhale_api/users/serializers.py
@@ -45,15 +45,7 @@ class UserActivitySerializer(activity_serializers.ModelSerializer):
         return "Person"
 
 
-avatar_field = VersatileImageFieldSerializer(
-    allow_null=True,
-    sizes=[
-        ("original", "url"),
-        ("square_crop", "crop__400x400"),
-        ("medium_square_crop", "crop__200x200"),
-        ("small_square_crop", "crop__50x50"),
-    ],
-)
+avatar_field = VersatileImageFieldSerializer(allow_null=True, sizes="square")
 
 
 class UserBasicSerializer(serializers.ModelSerializer):
diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py
index 0d7400df..8705354f 100644
--- a/api/tests/music/test_serializers.py
+++ b/api/tests/music/test_serializers.py
@@ -12,7 +12,12 @@ def test_artist_album_serializer(factories, to_api_date):
         "artist": album.artist.id,
         "creation_date": to_api_date(album.creation_date),
         "tracks_count": 1,
-        "cover": album.cover.url,
+        "cover": {
+            "original": album.cover.url,
+            "square_crop": album.cover.crop["400x400"].url,
+            "medium_square_crop": album.cover.crop["200x200"].url,
+            "small_square_crop": album.cover.crop["50x50"].url,
+        },
         "release_date": to_api_date(album.release_date),
     }
     serializer = serializers.ArtistAlbumSerializer(album)
@@ -83,7 +88,12 @@ def test_album_serializer(factories, to_api_date):
         "title": album.title,
         "artist": serializers.ArtistSimpleSerializer(album.artist).data,
         "creation_date": to_api_date(album.creation_date),
-        "cover": album.cover.url,
+        "cover": {
+            "original": album.cover.url,
+            "square_crop": album.cover.crop["400x400"].url,
+            "medium_square_crop": album.cover.crop["200x200"].url,
+            "small_square_crop": album.cover.crop["50x50"].url,
+        },
         "release_date": to_api_date(album.release_date),
         "tracks": serializers.AlbumTrackSerializer([track2, track1], many=True).data,
     }
diff --git a/api/tests/playlists/test_serializers.py b/api/tests/playlists/test_serializers.py
index 42569f7a..79765a24 100644
--- a/api/tests/playlists/test_serializers.py
+++ b/api/tests/playlists/test_serializers.py
@@ -80,11 +80,11 @@ def test_playlist_serializer_include_covers(factories, api_request):
     qs = playlist.__class__.objects.with_covers().with_tracks_count()
 
     expected = [
-        request.build_absolute_uri(t1.album.cover.url),
-        request.build_absolute_uri(t2.album.cover.url),
-        request.build_absolute_uri(t4.album.cover.url),
-        request.build_absolute_uri(t5.album.cover.url),
-        request.build_absolute_uri(t6.album.cover.url),
+        request.build_absolute_uri(t1.album.cover.crop["200x200"].url),
+        request.build_absolute_uri(t2.album.cover.crop["200x200"].url),
+        request.build_absolute_uri(t4.album.cover.crop["200x200"].url),
+        request.build_absolute_uri(t5.album.cover.crop["200x200"].url),
+        request.build_absolute_uri(t6.album.cover.crop["200x200"].url),
     ]
 
     serializer = serializers.PlaylistSerializer(qs.get(), context={"request": request})
diff --git a/changes/changelog.d/image.enhancement b/changes/changelog.d/image.enhancement
new file mode 100644
index 00000000..d0ad52c9
--- /dev/null
+++ b/changes/changelog.d/image.enhancement
@@ -0,0 +1,25 @@
+Use thumbnails for avatars and covers to reduce bandwidth
+
+
+Image thumbnails [Manual action required]
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To reduce bandwidth usage on slow or limited connexions and improve performance
+in general, we now use smaller images in the front-end. For instance, if you have
+an album cover with a 1000x1000 pixel size, we will create smaller
+versions of this image (50x50, 200x200, 400x400) and reference those resized version
+when we don't actually need the original image.
+
+Thumbnail will be created automatically for new objects, however, you have
+to launch a manual command to deal with existing ones.
+
+On docker setups::
+
+    docker-compose run --rm api python manage.py script create_image_variations --no-input
+
+On non-docker setups::
+
+    python manage.py script create_image_variations --no-input
+
+This should be quite fast but may take up to a few minutes depending on the number
+of albums you have in database. It is safe to interrupt the process or rerun it multiple times.
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 6f744d74..4a3f42c7 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -133,7 +133,7 @@
           <tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
               <td class="right aligned">{{ index + 1}}</td>
               <td class="center aligned">
-                  <img class="ui mini image" v-if="track.album.cover" :src="$store.getters['instance/absoluteUrl'](track.album.cover)">
+                  <img class="ui mini image" v-if="track.album.cover && track.album.cover.original" :src="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)">
                   <img class="ui mini image" v-else src="../assets/audio/default-cover.png">
               </td>
               <td colspan="4">
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 84b07620..704121d9 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -14,7 +14,7 @@
       <div v-if="currentTrack" class="track-area ui unstackable items">
         <div class="ui inverted item">
           <div class="ui tiny image">
-            <img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover)">
+            <img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover && currentTrack.album.cover.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.medium_square_crop)">
             <img v-else src="../../assets/audio/default-cover.png">
           </div>
           <div class="middle aligned content">
diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue
index 71f2ec80..0c5a7c80 100644
--- a/front/src/components/audio/album/Card.vue
+++ b/front/src/components/audio/album/Card.vue
@@ -2,7 +2,7 @@
     <div class="ui card">
       <div class="content">
         <div class="right floated tiny ui image">
-          <img v-if="album.cover" v-lazy="$store.getters['instance/absoluteUrl'](album.cover)">
+          <img v-if="album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](album.cover.square_crop)">
           <img v-else src="../../../assets/audio/default-cover.png">
         </div>
         <div class="header">
diff --git a/front/src/components/audio/album/Widget.vue b/front/src/components/audio/album/Widget.vue
index 37cddd50..fa71808b 100644
--- a/front/src/components/audio/album/Widget.vue
+++ b/front/src/components/audio/album/Widget.vue
@@ -3,9 +3,9 @@
     <h3 class="ui header">
       <slot name="title"></slot>
     </h3>
-    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle left', 'icon']">
+    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle left', 'icon']">
     </i>
-    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle right', 'icon']">
+    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle right', 'icon']">
     </i>
     <div class="ui hidden divider"></div>
     <div class="ui five cards">
@@ -13,7 +13,7 @@
         <div class="ui loader"></div>
       </div>
       <div class="card" v-for="album in albums" :key="album.id">
-        <div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover}]" :style="getImageStyle(album)">
+        <div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover.original}]" :style="getImageStyle(album)">
           <play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album.id"></play-button>
         </div>
         <div class="content">
@@ -92,8 +92,8 @@ export default {
     getImageStyle (album) {
       let url = '../../../assets/audio/default-cover.png'
 
-      if (album.cover) {
-        url = this.$store.getters['instance/absoluteUrl'](album.cover)
+      if (album.cover.original) {
+        url = this.$store.getters['instance/absoluteUrl'](album.cover.medium_square_crop)
       } else {
         return {}
       }
diff --git a/front/src/components/audio/artist/Card.vue b/front/src/components/audio/artist/Card.vue
index bd7cb68c..dd32b473 100644
--- a/front/src/components/audio/artist/Card.vue
+++ b/front/src/components/audio/artist/Card.vue
@@ -11,7 +11,7 @@
             <tbody>
               <tr v-for="album in albums">
                 <td>
-                  <img class="ui mini image" v-if="album.cover" :src="$store.getters['instance/absoluteUrl'](album.cover)">
+                  <img class="ui mini image" v-if="album.cover.original" :src="$store.getters['instance/absoluteUrl'](album.cover.small_square_crop)">
                   <img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
                 </td>
                 <td colspan="4">
diff --git a/front/src/components/audio/track/Row.vue b/front/src/components/audio/track/Row.vue
index ef3660ee..cf79267c 100644
--- a/front/src/components/audio/track/Row.vue
+++ b/front/src/components/audio/track/Row.vue
@@ -4,7 +4,7 @@
       <play-button class="basic icon" :discrete="true" :track="track"></play-button>
     </td>
     <td>
-      <img class="ui mini image" v-if="track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover)">
+      <img class="ui mini image" v-if="track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)">
       <img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
     </td>
     <td colspan="6">
diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue
index 7c727b40..ca3ae242 100644
--- a/front/src/components/audio/track/Widget.vue
+++ b/front/src/components/audio/track/Widget.vue
@@ -3,9 +3,11 @@
     <h3 class="ui header">
       <slot name="title"></slot>
     </h3>
-    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle up', 'icon']">
+    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle up', 'icon']">
     </i>
-    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle down', 'icon']">
+    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle down', 'icon']">
+    </i>
+    <i @click="fetchData(url)" :class="['ui', 'circular', 'medium', 'refresh', 'icon']">
     </i>
     <div class="ui divided unstackable items">
       <div v-if="isLoading" class="ui inverted active dimmer">
@@ -13,7 +15,7 @@
       </div>
       <div class="item" v-for="object in objects" :key="object.id">
         <div class="ui tiny image">
-          <img v-if="object.track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover)">
+          <img v-if="object.track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.medium_square_crop)">
           <img v-else src="../../../assets/audio/default-cover.png">
           <play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'tiny', 'orange', 'icon', 'button']" :track="object.track"></play-button>
         </div>
@@ -121,4 +123,7 @@ export default {
     left: 2.5em;
   }
 }
+.refresh.icon {
+  float: right;
+}
 </style>
diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue
index 698f07ae..312640ba 100644
--- a/front/src/components/library/Album.vue
+++ b/front/src/components/library/Album.vue
@@ -4,7 +4,7 @@
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <template v-if="album">
-      <div :class="['ui', 'head', {'with-background': album.cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="album.title">
+      <div :class="['ui', 'head', {'with-background': album.cover.original}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="album.title">
         <div class="segment-content">
           <h2 class="ui center aligned icon header">
             <i class="circular inverted sound yellow icon"></i>
@@ -98,10 +98,10 @@ export default {
       return 'https://musicbrainz.org/release/' + this.album.mbid
     },
     headerStyle () {
-      if (!this.album.cover) {
+      if (!this.album.cover.original) {
         return ''
       }
-      return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.album.cover) + ')'
+      return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.album.cover.original) + ')'
     }
   },
   watch: {
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index 6e3ad638..0f0abe1e 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -158,10 +158,10 @@ export default {
       })[0]
     },
     headerStyle () {
-      if (!this.cover) {
+      if (!this.cover.original) {
         return ''
       }
-      return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover) + ')'
+      return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover.original) + ')'
     }
   },
   watch: {
diff --git a/front/src/components/playlists/Widget.vue b/front/src/components/playlists/Widget.vue
index 72c54420..65904a12 100644
--- a/front/src/components/playlists/Widget.vue
+++ b/front/src/components/playlists/Widget.vue
@@ -3,9 +3,11 @@
     <h3 class="ui header">
       <slot name="title"></slot>
     </h3>
-    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle up', 'icon']">
+    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle up', 'icon']">
     </i>
-    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle down', 'icon']">
+    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle down', 'icon']">
+    </i>
+    <i @click="fetchData(url)" :class="['ui', 'circular', 'medium', 'refresh', 'icon']">
     </i>
     <div v-if="isLoading" class="ui inverted active dimmer">
       <div class="ui loader"></div>
@@ -75,3 +77,8 @@ export default {
   }
 }
 </script>
+<style scoped>
+.refresh.icon {
+  float: right;
+}
+</style>
-- 
GitLab