Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Extend image proxy to work with cover art #171

Merged
merged 12 commits into from
Aug 23, 2021
2 changes: 1 addition & 1 deletion load_sample_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ docker-compose exec -T "$UPSTREAM_DB_SERVICE_NAME" /bin/bash -c "psql -U deploy
docker-compose exec -T "$UPSTREAM_DB_SERVICE_NAME" /bin/bash -c "PGPASSWORD=deploy pg_dump -s -t audio -U deploy -d openledger -h db | head -n -14 | psql -U deploy -d openledger"
docker-compose exec -T "$UPSTREAM_DB_SERVICE_NAME" /bin/bash -c "psql -U deploy -d openledger <<-EOF
ALTER TABLE audio RENAME TO audio_view;
ALTER TABLE audio_view ADD COLUMN standardized_popularity double precision, ADD COLUMN ingestion_type varchar(1000), ADD COLUMN audio_set jsonb, ADD COLUMN thumbnail varchar(1000);
ALTER TABLE audio_view ADD COLUMN standardized_popularity double precision, ADD COLUMN ingestion_type varchar(1000), ADD COLUMN audio_set jsonb;
\copy audio_view (identifier,created_on,updated_on,ingestion_type,provider,source,foreign_identifier,foreign_landing_url,url,thumbnail,duration,bit_rate,sample_rate,category,genres,audio_set,alt_files,filesize,license,license_version,creator,creator_url,title,meta_data,tags,watermarked,last_synced_with_source,removed_from_source,standardized_popularity) from './sample_data/sample_audio_data.csv' with (FORMAT csv, HEADER true)
EOF"

Expand Down
23 changes: 23 additions & 0 deletions openverse-api/catalog/api/migrations/0037_media_thumbnails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.6 on 2021-08-23 11:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0036_foreign_id_uniq'),
]

operations = [
migrations.AddField(
model_name='audio',
name='thumbnail',
field=models.URLField(blank=True, help_text='The thumbnail for the media.', max_length=1000, null=True),
),
migrations.AlterField(
model_name='image',
name='thumbnail',
field=models.URLField(blank=True, help_text='The thumbnail for the media.', max_length=1000, null=True),
),
]
1 change: 1 addition & 0 deletions openverse-api/catalog/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
MatureAudio,
AudioList,
AltAudioFile,
AudioSet,
)
from catalog.api.models.media import (
PENDING,
Expand Down
7 changes: 0 additions & 7 deletions openverse-api/catalog/api/models/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@


class Image(AbstractMedia):
thumbnail = models.URLField(
max_length=1000,
blank=True,
null=True,
help_text="The thumbnail for the image, if any."
)

width = models.IntegerField(blank=True, null=True)
height = models.IntegerField(blank=True, null=True)

Expand Down
7 changes: 7 additions & 0 deletions openverse-api/catalog/api/models/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ class AbstractMedia(IdentifierMixin, MediaMixin, FileMixin, OpenLedgerModel):
information common to all media types indexed by Openverse.
"""

thumbnail = models.URLField(
max_length=1000,
blank=True,
null=True,
help_text="The thumbnail for the media."
)

watermarked = models.BooleanField(blank=True, null=True)

license = models.CharField(max_length=50)
Expand Down
10 changes: 10 additions & 0 deletions openverse-api/catalog/api/serializers/audio_serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.urls import reverse
from rest_framework import serializers

from catalog.api.controllers.search_controller import get_sources
Expand Down Expand Up @@ -95,6 +96,9 @@ class AudioSerializer(MediaSerializer):
used to generate Swagger documentation.
"""

thumbnail = serializers.SerializerMethodField(
help_text="A direct link to the miniature artwork."
)
audio_set = serializers.PrimaryKeyRelatedField(
required=False,
help_text='Reference to set of which this track is a part.',
Expand Down Expand Up @@ -141,6 +145,12 @@ class AudioSerializer(MediaSerializer):
help_text="A link to an endpoint that provides similar audio files."
)

def get_thumbnail(self, obj):
request = self.context['request']
host = request.get_host()
path = reverse('audio-thumb', kwargs={'identifier': obj.identifier})
return f'https://{host}{path}'


class AudioSearchResultsSerializer(MediaSearchResultsSerializer):
""" The full audio search response. """
Expand Down
12 changes: 0 additions & 12 deletions openverse-api/catalog/api/serializers/image_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,6 @@ def get_thumbnail(self, obj):
return f'https://{host}{path}'


class ProxiedImageSerializer(serializers.Serializer):
"""
We want to show 3rd party content securely and under our own native URLs, so
we route some images through our own proxy. We use this same endpoint to
generate thumbnails for content.
"""
full_size = serializers.BooleanField(
default=False,
help_text="If set, do not thumbnail the image."
)


class ImageSearchResultsSerializer(MediaSearchResultsSerializer):
""" The full image search response. """
results = ImageSerializer(
Expand Down
12 changes: 12 additions & 0 deletions openverse-api/catalog/api/serializers/media_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,15 @@ class AboutMediaSerializer(serializers.Serializer):
source_url = serializers.CharField(
help_text="The actual URL to the `source_name`."
)


class ProxiedImageSerializer(serializers.Serializer):
"""
We want to show 3rd party content securely and under our own native URLs, so
we route some images through our own proxy. We use this same endpoint to
generate thumbnails for content.
"""
full_size = serializers.BooleanField(
default=False,
help_text="If set, do not thumbnail the image."
)
31 changes: 31 additions & 0 deletions openverse-api/catalog/api/views/audio_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
audio_stats_200_example,
)
from catalog.api.models import Audio, AudioReport
from catalog.api.serializers.media_serializers import ProxiedImageSerializer
from catalog.api.serializers.audio_serializers import (
AudioSearchQueryStringSerializer,
AudioSearchResultsSerializer,
Expand All @@ -40,6 +41,7 @@
RelatedMedia,
MediaDetail,
MediaStats,
ImageProxy,
)
from catalog.custom_auto_schema import CustomAutoSchema

Expand Down Expand Up @@ -237,3 +239,32 @@ class AudioStats(MediaStats):
])
def get(self, request, format=None):
return self._get(request, 'audio')


class AudioArt(ImageProxy):
"""
Return the thumbnail of the artwork of the audio. This returns the thumbnail
of the audio, falling back to the thumbnail of the audio set.
"""

queryset = Audio.objects.all()

def get(self, request, identifier, format=None):
serialized = ProxiedImageSerializer(data=request.data)
serialized.is_valid()
try:
audio = Audio.objects.get(identifier=identifier)
image_url = audio.thumbnail
if not image_url:
image_url = audio.audio_set.url
except Audio.DoesNotExist:
return Response(status=404, data='Audio not found')
except AttributeError:
return Response(status=404, data='Audio set not found')
if not image_url:
return Response(status=404, data='Cover art URL not found')

if serialized.data['full_size']:
return self._get(image_url, None)
else:
return self._get(image_url)
34 changes: 6 additions & 28 deletions openverse-api/catalog/api/views/image_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import piexif
import requests
from PIL import Image as img
from django.conf import settings
from django.urls import reverse
from django.http.response import HttpResponse, FileResponse
from drf_yasg import openapi
Expand All @@ -14,8 +13,6 @@
from rest_framework.generics import GenericAPIView, CreateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from urllib.error import HTTPError
from urllib.request import urlopen

import catalog.api.controllers.search_controller as search_controller
from catalog.api.examples import (
Expand All @@ -40,6 +37,7 @@
InputErrorSerializer,
NotFoundErrorSerializer,
)
from catalog.api.serializers.media_serializers import ProxiedImageSerializer
from catalog.api.serializers.image_serializers import (
ImageSearchQueryStringSerializer,
ImageSearchResultsSerializer,
Expand All @@ -49,10 +47,8 @@
OembedSerializer,
OembedResponseSerializer,
AboutImageSerializer,
ProxiedImageSerializer,
)
from catalog.api.utils import ccrel
from catalog.api.utils.throttle import OneThousandPerMinute
from catalog.api.utils.exceptions import input_error_response
from catalog.api.utils.watermark import watermark
from catalog.api.views.media_views import (
Expand All @@ -65,6 +61,7 @@
RelatedMedia,
MediaDetail,
MediaStats,
ImageProxy,
)
from catalog.custom_auto_schema import CustomAutoSchema

Expand Down Expand Up @@ -436,42 +433,23 @@ def get(self, request, format=None):
return self._get(request, 'image')


class ProxiedImage(APIView):
class ProxiedImage(ImageProxy):
"""
Return the thumb of an image.
"""

lookup_field = 'identifier'
queryset = Image.objects.all()
throttle_classes = [OneThousandPerMinute]
swagger_schema = None

def get(self, request, identifier, format=None):
serialized = ProxiedImageSerializer(data=request.data)
serialized.is_valid()
try:
image = Image.objects.get(identifier=identifier)
image_url = image.url
except Image.DoesNotExist:
return Response(status=404, data='Not Found')

if serialized.data['full_size']:
proxy_upstream = f'{settings.THUMBNAIL_PROXY_URL}/{image.url}'
return self._get(image_url, None)
else:
proxy_upstream = f'{settings.THUMBNAIL_PROXY_URL}/' \
f'{settings.THUMBNAIL_WIDTH_PX}' \
f',fit/{image.url}'
try:
upstream_response = urlopen(proxy_upstream)
status = upstream_response.status
content_type = upstream_response.headers.get('Content-Type')
except HTTPError:
log.info('Failed to render thumbnail: ', exc_info=True)
return HttpResponse(status=500)

response = HttpResponse(
upstream_response.read(),
status=status,
content_type=content_type
)

return response
return self._get(image_url)
39 changes: 39 additions & 0 deletions openverse-api/catalog/api/views/media_views.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import logging

from django.conf import settings
from django.http.response import HttpResponse
from rest_framework.authentication import BasicAuthentication
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from rest_framework.views import APIView
from urllib.error import HTTPError
from urllib.request import urlopen

from catalog.api.controllers import search_controller
from catalog.api.controllers.search_controller import get_sources
from catalog.api.models import ContentProvider, SourceLogo
from catalog.api.utils.exceptions import input_error_response
from catalog.api.utils.throttle import OneThousandPerMinute
from catalog.custom_auto_schema import CustomAutoSchema

log = logging.getLogger(__name__)

FOREIGN_LANDING_URL = 'foreign_landing_url'
CREATOR_URL = 'creator_url'
RESULTS = 'results'
Expand Down Expand Up @@ -188,3 +197,33 @@ def _get(self, request, index_name):
}
)
return Response(status=200, data=response)


class ImageProxy(APIView):
swagger_schema = None

lookup_field = 'identifier'
throttle_classes = [OneThousandPerMinute]

def _get(self, media_url, width=settings.THUMBNAIL_WIDTH_PX):
if width is None: # full size
proxy_upstream = f'{settings.THUMBNAIL_PROXY_URL}/{media_url}'
else:
proxy_upstream = f'{settings.THUMBNAIL_PROXY_URL}/' \
f'{settings.THUMBNAIL_WIDTH_PX},fit/' \
f'{media_url}'
try:
upstream_response = urlopen(proxy_upstream)
status = upstream_response.status
content_type = upstream_response.headers.get('Content-Type')
except HTTPError:
log.info('Failed to render thumbnail: ', exc_info=True)
return HttpResponse(status=500)

response = HttpResponse(
upstream_response.read(),
status=status,
content_type=content_type
)

return response
6 changes: 6 additions & 0 deletions openverse-api/catalog/urls/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
AudioDetail,
RelatedAudio,
AudioStats,
AudioArt,
)

urlpatterns = [
Expand All @@ -18,6 +19,11 @@
AudioDetail.as_view(),
name='audio-detail'
),
path(
'<str:identifier>/thumb',
AudioArt.as_view(),
name='audio-thumb'
),
path(
'<str:identifier>/recommendations',
RelatedAudio.as_view(),
Expand Down
6 changes: 6 additions & 0 deletions openverse-api/test/audio_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
search_consistency,
detail,
stats,
thumb,
)


Expand Down Expand Up @@ -50,3 +51,8 @@ def test_audio_detail(audio_fixture):

def test_audio_stats():
stats('audio', 'audio_count')


@pytest.mark.skip(reason='No images have audio set image yet')
def test_audio_thumb(audio_fixture):
thumb(audio_fixture)
5 changes: 5 additions & 0 deletions openverse-api/test/image_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
search_consistency,
detail,
stats,
thumb,
)


Expand Down Expand Up @@ -53,6 +54,10 @@ def test_image_stats():
stats('images', 'image_count')


def test_image_thumb(image_fixture):
thumb(image_fixture)


def test_oembed_endpoint_for_json():
params = {
'url': 'https://any.domain/any/path/29cb352c-60c1-41d8-bfa1-7d6f7d955f63',
Expand Down
7 changes: 7 additions & 0 deletions openverse-api/test/media_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,10 @@ def stats(media_type, count_key):
provider_count += 1
assert num_media > 0
assert provider_count > 0


def thumb(fixture):
thumbnail_url = fixture['results'][0]['thumbnail'].replace('https:', 'http:')
thumbnail_response = requests.get(thumbnail_url)
assert thumbnail_response.status_code == 200
assert thumbnail_response.headers["Content-Type"].startswith("image/")
Loading