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 USE_TZ = True
# Thumbnails
THUMBNAIL_SIZE_VIDEO = (410, 230)
THUMBNAIL_SIZE_SUBSCRIPTION = (250, 250)
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/ # https://docs.djangoproject.com/en/1.11/howto/static-files/

View File

@ -6,6 +6,8 @@ import logging
import requests import requests
import mimetypes import mimetypes
import os import os
import PIL.Image
import PIL.ImageOps
from urllib.parse import urljoin from urllib.parse import urljoin
log = logging.getLogger('downloader') log = logging.getLogger('downloader')
@ -61,9 +63,9 @@ def downloader_process_all():
downloader_process_subscription(subscription) 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 # Make request to obtain mime type
try: try:
@ -75,17 +77,28 @@ def fetch_thumbnail(url, object_type, identifier, quality):
ext = mimetypes.guess_extension(response.headers['Content-Type']) ext = mimetypes.guess_extension(response.headers['Content-Type'])
# Build file path # 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_dir = os.path.join(srv_settings.MEDIA_ROOT, "thumbs", object_type)
abs_path = os.path.join(abs_path_dir, file_name) abs_path = os.path.join(abs_path_dir, file_name)
abs_path_tmp = file_name + '.tmp'
# Store image # Store image
try: try:
os.makedirs(abs_path_dir, exist_ok=True) 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): for chunk in response.iter_content(chunk_size=1024):
if chunk: if chunk:
f.write(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: except requests.exceptions.RequestException as e:
log.error('Error while downloading stream for thumbnail %s. Error: %s', url, e) log.error('Error while downloading stream for thumbnail %s. Error: %s', url, e)
return url return url

View File

@ -4,6 +4,7 @@ from threading import Lock
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from django.db.models import Max from django.db.models import Max
from django.conf import settings
from YtManagerApp.management.appconfig import appconfig from YtManagerApp.management.appconfig import appconfig
from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_subscription 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)) self.__new_vids.append(Video.create(item, sub))
def fetch_missing_thumbnails(self, object: Union[Subscription, Video]): def fetch_missing_thumbnails(self, obj: Union[Subscription, Video]):
if isinstance(object, Subscription): if obj.thumbnail.startswith("http"):
object_type = "sub" if isinstance(obj, Subscription):
object_id = object.playlist_id obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'sub', obj.playlist_id, settings.THUMBNAIL_SIZE_SUBSCRIPTION)
else: elif isinstance(obj, Video):
object_type = "video" obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'video', obj.video_id, settings.THUMBNAIL_SIZE_VIDEO)
object_id = object.video_id obj.save()
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 check_video_deleted(self, video: Video): def check_video_deleted(self, video: Video):
if video.downloaded_path is not None: 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() description = models.TextField()
channel_id = models.CharField(max_length=128) channel_id = models.CharField(max_length=128)
channel_name = models.CharField(max_length=1024) channel_name = models.CharField(max_length=1024)
icon_default = models.CharField(max_length=1024) thumbnail = models.CharField(max_length=1024)
icon_best = models.CharField(max_length=1024)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
# youtube adds videos to the 'Uploads' playlist at the top instead of the bottom # youtube adds videos to the 'Uploads' playlist at the top instead of the bottom
rewrite_playlist_indices = models.BooleanField(null=False, default=False) rewrite_playlist_indices = models.BooleanField(null=False, default=False)
@ -133,8 +132,7 @@ class Subscription(models.Model):
self.description = info_playlist.description self.description = info_playlist.description
self.channel_id = info_playlist.channel_id self.channel_id = info_playlist.channel_id
self.channel_name = info_playlist.channel_title self.channel_name = info_playlist.channel_title
self.icon_default = youtube.default_thumbnail(info_playlist).url self.thumbnail = youtube.best_thumbnail(info_playlist).url
self.icon_best = youtube.best_thumbnail(info_playlist).url
def copy_from_channel(self, info_channel: youtube.Channel): def copy_from_channel(self, info_channel: youtube.Channel):
# No point in storing info about the 'uploads from X' playlist # 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.description = info_channel.description
self.channel_id = info_channel.id self.channel_id = info_channel.id
self.channel_name = info_channel.title self.channel_name = info_channel.title
self.icon_default = youtube.default_thumbnail(info_channel).url self.thumbnail = youtube.best_thumbnail(info_channel).url
self.icon_best = youtube.best_thumbnail(info_channel).url
self.rewrite_playlist_indices = True self.rewrite_playlist_indices = True
def fetch_from_url(self, url, yt_api: youtube.YoutubeAPI): 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) subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
playlist_index = models.IntegerField(null=False) playlist_index = models.IntegerField(null=False)
publish_date = models.DateTimeField(null=False) publish_date = models.DateTimeField(null=False)
icon_default = models.TextField() thumbnail = models.TextField()
icon_best = models.TextField()
uploader_name = models.TextField(null=False) uploader_name = models.TextField(null=False)
views = models.IntegerField(null=False, default=0) views = models.IntegerField(null=False, default=0)
rating = models.FloatField(null=False, default=0.5) rating = models.FloatField(null=False, default=0.5)
@ -194,8 +190,7 @@ class Video(models.Model):
video.subscription = subscription video.subscription = subscription
video.playlist_index = playlist_item.position video.playlist_index = playlist_item.position
video.publish_date = playlist_item.published_at video.publish_date = playlist_item.published_at
video.icon_default = youtube.default_thumbnail(playlist_item).url video.thumbnail = youtube.best_thumbnail(playlist_item).url
video.icon_best = youtube.best_thumbnail(playlist_item).url
video.save() video.save()
return video return video

View File

@ -7,7 +7,7 @@
<div class="card-wrapper d-flex align-items-stretch" style="width: 18rem;"> <div class="card-wrapper d-flex align-items-stretch" style="width: 18rem;">
<div class="card mx-auto"> <div class="card mx-auto">
<a href="{% url 'video' video.id %}" target="_blank"> <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> </a>
<div class="card-body"> <div class="card-body">
<h5 class="card-title"> <h5 class="card-title">

View File

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

View File

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

View File

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