Simplified thumbnail logic, only the best thumbnail size is downloaded and then it is resized and cropped to an appropriate size for use in the web UI.

This commit is contained in:
Tiberiu Chibici 2019-08-19 21:05:13 +03:00
parent 2bdafa291d
commit c1473dd163
10 changed files with 82 additions and 33 deletions

View File

@ -105,6 +105,9 @@ USE_L10N = True
USE_TZ = True
# Thumbnails
THUMBNAIL_SIZE_VIDEO = (410, 230)
THUMBNAIL_SIZE_SUBSCRIPTION = (250, 250)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/

View File

@ -6,6 +6,8 @@ import logging
import requests
import mimetypes
import os
import PIL.Image
import PIL.ImageOps
from urllib.parse import urljoin
log = logging.getLogger('downloader')
@ -61,9 +63,9 @@ def downloader_process_all():
downloader_process_subscription(subscription)
def fetch_thumbnail(url, object_type, identifier, quality):
def fetch_thumbnail(url, object_type, identifier, thumb_size):
log.info('Fetching thumbnail url=%s object_type=%s identifier=%s quality=%s', url, object_type, identifier, quality)
log.info('Fetching thumbnail url=%s object_type=%s identifier=%s', url, object_type, identifier)
# Make request to obtain mime type
try:
@ -75,17 +77,28 @@ def fetch_thumbnail(url, object_type, identifier, quality):
ext = mimetypes.guess_extension(response.headers['Content-Type'])
# Build file path
file_name = f"{identifier}-{quality}{ext}"
file_name = f"{identifier}{ext}"
abs_path_dir = os.path.join(srv_settings.MEDIA_ROOT, "thumbs", object_type)
abs_path = os.path.join(abs_path_dir, file_name)
abs_path_tmp = file_name + '.tmp'
# Store image
try:
os.makedirs(abs_path_dir, exist_ok=True)
with open(abs_path, "wb") as f:
with open(abs_path_tmp, "wb") as f:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
# Resize and crop to thumbnail size
image = PIL.Image.open(abs_path_tmp)
image = PIL.ImageOps.fit(image, thumb_size)
image.save(abs_path)
image.close()
# Delete temp file
os.unlink(abs_path_tmp)
except requests.exceptions.RequestException as e:
log.error('Error while downloading stream for thumbnail %s. Error: %s', url, e)
return url

View File

@ -4,6 +4,7 @@ from threading import Lock
from apscheduler.triggers.cron import CronTrigger
from django.db.models import Max
from django.conf import settings
from YtManagerApp.management.appconfig import appconfig
from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_subscription
@ -110,21 +111,13 @@ class SynchronizeJob(Job):
self.__new_vids.append(Video.create(item, sub))
def fetch_missing_thumbnails(self, object: Union[Subscription, Video]):
if isinstance(object, Subscription):
object_type = "sub"
object_id = object.playlist_id
else:
object_type = "video"
object_id = object.video_id
if object.icon_default.startswith("http"):
object.icon_default = fetch_thumbnail(object.icon_default, object_type, object_id, 'default')
object.save()
if object.icon_best.startswith("http"):
object.icon_best = fetch_thumbnail(object.icon_best, object_type, object_id, 'best')
object.save()
def fetch_missing_thumbnails(self, obj: Union[Subscription, Video]):
if obj.thumbnail.startswith("http"):
if isinstance(obj, Subscription):
obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'sub', obj.playlist_id, settings.THUMBNAIL_SIZE_SUBSCRIPTION)
elif isinstance(obj, Video):
obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'video', obj.video_id, settings.THUMBNAIL_SIZE_VIDEO)
obj.save()
def check_video_deleted(self, video: Video):
if video.downloaded_path is not None:

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.4 on 2019-08-19 16:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('YtManagerApp', '0010_auto_20190819_1317'),
]
operations = [
migrations.RemoveField(
model_name='subscription',
name='icon_default',
),
migrations.RemoveField(
model_name='video',
name='icon_default',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-08-19 16:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('YtManagerApp', '0011_auto_20190819_1613'),
]
operations = [
migrations.RenameField(
model_name='subscription',
old_name='icon_best',
new_name='thumbnail',
),
migrations.RenameField(
model_name='video',
old_name='icon_best',
new_name='thumbnail',
),
]

View File

@ -106,8 +106,7 @@ class Subscription(models.Model):
description = models.TextField()
channel_id = models.CharField(max_length=128)
channel_name = models.CharField(max_length=1024)
icon_default = models.CharField(max_length=1024)
icon_best = models.CharField(max_length=1024)
thumbnail = models.CharField(max_length=1024)
user = models.ForeignKey(User, on_delete=models.CASCADE)
# youtube adds videos to the 'Uploads' playlist at the top instead of the bottom
rewrite_playlist_indices = models.BooleanField(null=False, default=False)
@ -133,8 +132,7 @@ class Subscription(models.Model):
self.description = info_playlist.description
self.channel_id = info_playlist.channel_id
self.channel_name = info_playlist.channel_title
self.icon_default = youtube.default_thumbnail(info_playlist).url
self.icon_best = youtube.best_thumbnail(info_playlist).url
self.thumbnail = youtube.best_thumbnail(info_playlist).url
def copy_from_channel(self, info_channel: youtube.Channel):
# No point in storing info about the 'uploads from X' playlist
@ -143,8 +141,7 @@ class Subscription(models.Model):
self.description = info_channel.description
self.channel_id = info_channel.id
self.channel_name = info_channel.title
self.icon_default = youtube.default_thumbnail(info_channel).url
self.icon_best = youtube.best_thumbnail(info_channel).url
self.thumbnail = youtube.best_thumbnail(info_channel).url
self.rewrite_playlist_indices = True
def fetch_from_url(self, url, yt_api: youtube.YoutubeAPI):
@ -176,8 +173,7 @@ class Video(models.Model):
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
playlist_index = models.IntegerField(null=False)
publish_date = models.DateTimeField(null=False)
icon_default = models.TextField()
icon_best = models.TextField()
thumbnail = models.TextField()
uploader_name = models.TextField(null=False)
views = models.IntegerField(null=False, default=0)
rating = models.FloatField(null=False, default=0.5)
@ -194,8 +190,7 @@ class Video(models.Model):
video.subscription = subscription
video.playlist_index = playlist_item.position
video.publish_date = playlist_item.published_at
video.icon_default = youtube.default_thumbnail(playlist_item).url
video.icon_best = youtube.best_thumbnail(playlist_item).url
video.thumbnail = youtube.best_thumbnail(playlist_item).url
video.save()
return video

View File

@ -7,7 +7,7 @@
<div class="card-wrapper d-flex align-items-stretch" style="width: 18rem;">
<div class="card mx-auto">
<a href="{% url 'video' video.id %}" target="_blank">
<img class="card-img-top" src="{{ video.icon_best }}" alt="Thumbnail">
<img class="card-img-top" src="{{ video.thumbnail }}" alt="Thumbnail">
</a>
<div class="card-body">
<h5 class="card-title">

View File

@ -37,7 +37,6 @@
{{ object.description | linebreaks | urlize }}
</div>
</p>
</div>
<div class="col-4">

View File

@ -144,7 +144,7 @@ def ajax_get_tree(request: HttpRequest):
"id": __tree_sub_id(node.id),
"type": "sub",
"text": node.name,
"icon": node.icon_default,
"icon": node.thumbnail,
"parent": __tree_folder_id(node.parent_folder_id)
}

View File

@ -1,3 +1,4 @@
pytz
requests
apscheduler
gunicorn
@ -10,4 +11,5 @@ google-api-python-client
google_auth_oauthlib
oauth2client
psycopg2-binary
python-dateutil
python-dateutil
Pillow