From e9d0599569fefd69b848d135cd33be9c07b2b805 Mon Sep 17 00:00:00 2001 From: administrator <7dn1yh5j@debauchez.fr> Date: Sun, 10 Dec 2023 19:06:00 +0100 Subject: [PATCH] Add option to export channel thumbnails for Jellyfin --- Pipfile | 1 + .../0021_source_copy_channel_thumbnails.py | 18 +++++++++ tubesync/sync/models.py | 16 ++++++++ tubesync/sync/signals.py | 8 +++- tubesync/sync/tasks.py | 37 +++++++++++++++++++ tubesync/sync/views.py | 4 +- 6 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 tubesync/sync/migrations/0021_source_copy_channel_thumbnails.py diff --git a/Pipfile b/Pipfile index bb2f955..090b4b5 100644 --- a/Pipfile +++ b/Pipfile @@ -23,3 +23,4 @@ yt-dlp = "*" redis = "*" hiredis = "*" requests = {extras = ["socks"], version = "*"} +bs4 = "*" \ No newline at end of file diff --git a/tubesync/sync/migrations/0021_source_copy_channel_thumbnails.py b/tubesync/sync/migrations/0021_source_copy_channel_thumbnails.py new file mode 100644 index 0000000..6c59dee --- /dev/null +++ b/tubesync/sync/migrations/0021_source_copy_channel_thumbnails.py @@ -0,0 +1,18 @@ +# Generated by nothing. Done manually by InterN0te on 2023-12-10 16:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sync', '0020_auto_20231024_1825'), + ] + + operations = [ + migrations.AddField( + model_name='source', + name='copy_channel_thumbnails', + field=models.BooleanField(default=False, help_text='Copy channel thumbnails in poster.jpg and season-poster.jpg, these may be detected and used by some media servers', verbose_name='copy channel thumbnails'), + ), + ] diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index dff8063..e05ef1b 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -2,6 +2,8 @@ import os import uuid import json import re +import requests +from bs4 import BeautifulSoup from xml.etree import ElementTree from collections import OrderedDict from datetime import datetime, timedelta @@ -342,6 +344,11 @@ class Source(models.Model): default=FALLBACK_NEXT_BEST_HD, help_text=_('What do do when media in your source resolution and codecs is not available') ) + copy_channel_thumbnails = models.BooleanField( + _('copy channel thumbnails'), + default=False, + help_text=_('Copy channel thumbnails in poster.jpg and season-poster.jpg, these may be detected and used by some media servers') + ) copy_thumbnails = models.BooleanField( _('copy thumbnails'), default=False, @@ -482,6 +489,15 @@ class Source(models.Model): def make_directory(self): return os.makedirs(self.directory_path, exist_ok=True) + @property + def get_thumbnail_url(self): + if self.source_type==Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: + raise Exception('This source is a playlist so it doesn\'t have thumbnail.') + soup = BeautifulSoup(requests.get(self.url, cookies={'CONSENT': 'YES+1'}).text, "html.parser") + data = re.search(r"var ytInitialData = ({.*});", str(soup.prettify())).group(1) + json_data = json.loads(data) + return json_data["header"]["c4TabbedHeaderRenderer"]["avatar"]["thumbnails"][2]["url"] + def directory_exists(self): return (os.path.isdir(self.directory_path) and os.access(self.directory_path, os.W_OK)) diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py index b92390e..abb212c 100644 --- a/tubesync/sync/signals.py +++ b/tubesync/sync/signals.py @@ -10,7 +10,7 @@ from .models import Source, Media, MediaServer from .tasks import (delete_task_by_source, delete_task_by_media, index_source_task, download_media_thumbnail, download_media_metadata, map_task_to_instance, check_source_directory_exists, - download_media, rescan_media_server) + download_media, rescan_media_server, download_source_thumbnail) from .utils import delete_file @@ -47,6 +47,12 @@ def source_post_save(sender, instance, created, **kwargs): priority=0, verbose_name=verbose_name.format(instance.name) ) + if instance.source_type != Source.SOURCE_TYPE_YOUTUBE_PLAYLIST and instance.copy_channel_thumbnails: + download_source_thumbnail( + str(instance.pk), + priority=0, + verbose_name=verbose_name.format(instance.name) + ) if instance.index_schedule > 0: delete_task_by_source('sync.tasks.index_source_task', instance.pk) log.info(f'Scheduling media indexing for source: {instance.name}') diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 5ecfd5e..87807b5 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -14,6 +14,7 @@ from datetime import timedelta, datetime from shutil import copyfile from PIL import Image from django.conf import settings +from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import timezone from django.db.utils import IntegrityError @@ -219,6 +220,42 @@ def check_source_directory_exists(source_id): source.make_directory() +@background(schedule=0) +def download_source_thumbnail(source_id): + ''' + Downloads an image and save it as a local thumbnail attached to a + Source instance. + ''' + try: + source = Source.objects.get(pk=source_id) + except Source.DoesNotExist: + # Task triggered but the source no longer exists, do nothing + log.error(f'Task download_source_thumbnail(pk={source_id}) called but no ' + f'source exists with ID: {source_id}') + return + + url = source.get_thumbnail_url + width = 400 + height = 400 + i = get_remote_image(url) + log.info(f'Resizing {i.width}x{i.height} thumbnail to ' + f'{width}x{height}: {url}') + i = resize_image_to_height(i, width, height) + image_file = BytesIO() + i.save(image_file, 'JPEG', quality=85, optimize=True, progressive=True) + + for file_name in ["poster.jpg", "season-poster.jpg"]: + # Reset file pointer to the beginning for the next save + image_file.seek(0) + # Create a Django ContentFile from BytesIO stream + django_file = ContentFile(image_file.read()) + file_path = source.directory_path / file_name + with open(file_path, 'wb') as f: + f.write(django_file.read()) + + log.info(f'Thumbnail downloaded from {url} for source with ID: {source_id}') + + @background(schedule=0) def download_media_metadata(media_id): ''' diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index 0b808eb..af0ded5 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -297,8 +297,8 @@ class EditSourceMixin: fields = ('source_type', 'key', 'name', 'directory', 'filter_text', 'media_format', 'index_schedule', 'download_media', 'download_cap', 'delete_old_media', 'delete_removed_media', 'days_to_keep', 'source_resolution', 'source_vcodec', - 'source_acodec', 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_thumbnails', - 'write_nfo', 'write_json', 'embed_metadata', 'embed_thumbnail', + 'source_acodec', 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_channel_thumbnails', + 'copy_thumbnails', 'write_nfo', 'write_json', 'embed_metadata', 'embed_thumbnail', 'enable_sponsorblock', 'sponsorblock_categories', 'write_subtitles', 'auto_subtitles', 'sub_langs') errors = {