mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
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:
parent
2bdafa291d
commit
c1473dd163
@ -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/
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
21
app/YtManagerApp/migrations/0011_auto_20190819_1613.py
Normal file
21
app/YtManagerApp/migrations/0011_auto_20190819_1613.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
23
app/YtManagerApp/migrations/0012_auto_20190819_1615.py
Normal file
23
app/YtManagerApp/migrations/0012_auto_20190819_1615.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pytz
|
||||||
requests
|
requests
|
||||||
apscheduler
|
apscheduler
|
||||||
gunicorn
|
gunicorn
|
||||||
@ -11,3 +12,4 @@ google_auth_oauthlib
|
|||||||
oauth2client
|
oauth2client
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
python-dateutil
|
python-dateutil
|
||||||
|
Pillow
|
Loading…
Reference in New Issue
Block a user