diff --git a/app/YtManager/settings.py b/app/YtManager/settings.py index 334879e..18313af 100644 --- a/app/YtManager/settings.py +++ b/app/YtManager/settings.py @@ -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/ diff --git a/app/YtManagerApp/management/downloader.py b/app/YtManagerApp/management/downloader.py index 41ea0b6..d206ed4 100644 --- a/app/YtManagerApp/management/downloader.py +++ b/app/YtManagerApp/management/downloader.py @@ -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 diff --git a/app/YtManagerApp/management/jobs/synchronize.py b/app/YtManagerApp/management/jobs/synchronize.py index d87f904..71cc34e 100644 --- a/app/YtManagerApp/management/jobs/synchronize.py +++ b/app/YtManagerApp/management/jobs/synchronize.py @@ -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: diff --git a/app/YtManagerApp/migrations/0011_auto_20190819_1613.py b/app/YtManagerApp/migrations/0011_auto_20190819_1613.py new file mode 100644 index 0000000..8c0abcb --- /dev/null +++ b/app/YtManagerApp/migrations/0011_auto_20190819_1613.py @@ -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', + ), + ] diff --git a/app/YtManagerApp/migrations/0012_auto_20190819_1615.py b/app/YtManagerApp/migrations/0012_auto_20190819_1615.py new file mode 100644 index 0000000..cd1b1a2 --- /dev/null +++ b/app/YtManagerApp/migrations/0012_auto_20190819_1615.py @@ -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', + ), + ] diff --git a/app/YtManagerApp/models.py b/app/YtManagerApp/models.py index bc04f6c..e40ee07 100644 --- a/app/YtManagerApp/models.py +++ b/app/YtManagerApp/models.py @@ -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 diff --git a/app/YtManagerApp/templates/YtManagerApp/index_videos.html b/app/YtManagerApp/templates/YtManagerApp/index_videos.html index c63ea1d..22d5ed3 100644 --- a/app/YtManagerApp/templates/YtManagerApp/index_videos.html +++ b/app/YtManagerApp/templates/YtManagerApp/index_videos.html @@ -7,7 +7,7 @@
- Thumbnail + Thumbnail
diff --git a/app/YtManagerApp/templates/YtManagerApp/video.html b/app/YtManagerApp/templates/YtManagerApp/video.html index 6b604d7..6e5b359 100644 --- a/app/YtManagerApp/templates/YtManagerApp/video.html +++ b/app/YtManagerApp/templates/YtManagerApp/video.html @@ -37,7 +37,6 @@ {{ object.description | linebreaks | urlize }}
-

diff --git a/app/YtManagerApp/views/index.py b/app/YtManagerApp/views/index.py index 1c93f8d..57864f2 100644 --- a/app/YtManagerApp/views/index.py +++ b/app/YtManagerApp/views/index.py @@ -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) } diff --git a/requirements.txt b/requirements.txt index ef9a6d0..d120bba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +pytz requests apscheduler gunicorn @@ -10,4 +11,5 @@ google-api-python-client google_auth_oauthlib oauth2client psycopg2-binary -python-dateutil \ No newline at end of file +python-dateutil +Pillow \ No newline at end of file