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 01/10] 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 = { From 7b2bb3dca63846034650d8c07df1bc44a4b62dd4 Mon Sep 17 00:00:00 2001 From: administrator <7dn1yh5j@debauchez.fr> Date: Mon, 11 Dec 2023 15:53:11 +0100 Subject: [PATCH 02/10] Add Try/Except block --- tubesync/sync/models.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 0fa3dde..8ca621b 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -491,12 +491,25 @@ class Source(models.Model): @property def get_thumbnail_url(self): - if self.source_type==Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: + if self.source_type == self.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"] + + try: + response = requests.get(self.url, cookies={'CONSENT': 'YES+1'}) + response.raise_for_status() + except RequestException as e: + print(f"Error occurred while making a request to YouTube: {e}") + return None + + soup = BeautifulSoup(response.text, "html.parser") + + try: + 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"] + except (KeyError, ValueError, TypeError) as e: + print(f"Error occurred while parsing YouTube JSON: {e}") + return None def directory_exists(self): return (os.path.isdir(self.directory_path) and From 8770c76d6b1b9a88669fe665d7ec883eaf8b88e2 Mon Sep 17 00:00:00 2001 From: administrator <7dn1yh5j@debauchez.fr> Date: Mon, 11 Dec 2023 18:50:31 +0100 Subject: [PATCH 03/10] Remove BeautifulSoup to only use YT-DLP --- Pipfile | 3 +- ...> 0021_source_copy_channel_thumbnails2.py} | 36 ++++++++--------- tubesync/sync/models.py | 22 ++--------- tubesync/sync/tasks.py | 39 ++++++++++++------- tubesync/sync/youtube.py | 29 ++++++++++++++ 5 files changed, 78 insertions(+), 51 deletions(-) rename tubesync/sync/migrations/{0021_source_copy_channel_thumbnails.py => 0021_source_copy_channel_thumbnails2.py} (97%) diff --git a/Pipfile b/Pipfile index 090b4b5..bfe7a90 100644 --- a/Pipfile +++ b/Pipfile @@ -22,5 +22,4 @@ mysqlclient = "*" yt-dlp = "*" redis = "*" hiredis = "*" -requests = {extras = ["socks"], version = "*"} -bs4 = "*" \ No newline at end of file +requests = {extras = ["socks"], version = "*"} \ 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_thumbnails2.py similarity index 97% rename from tubesync/sync/migrations/0021_source_copy_channel_thumbnails.py rename to tubesync/sync/migrations/0021_source_copy_channel_thumbnails2.py index 6c59dee..178ac97 100644 --- a/tubesync/sync/migrations/0021_source_copy_channel_thumbnails.py +++ b/tubesync/sync/migrations/0021_source_copy_channel_thumbnails2.py @@ -1,18 +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'), - ), - ] +# 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 8ca621b..9efe026 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -3,7 +3,6 @@ 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 @@ -18,7 +17,8 @@ from django.utils.translation import gettext_lazy as _ from common.errors import NoFormatException from common.utils import clean_filename from .youtube import (get_media_info as get_youtube_media_info, - download_media as download_youtube_media) + download_media as download_youtube_media, + get_channel_image_info as get_youtube_channel_image_info) from .utils import seconds_to_timestr, parse_media_format from .matching import (get_best_combined_format, get_best_audio_format, get_best_video_format) @@ -490,26 +490,12 @@ class Source(models.Model): return os.makedirs(self.directory_path, exist_ok=True) @property - def get_thumbnail_url(self): + def get_image_url(self): if self.source_type == self.SOURCE_TYPE_YOUTUBE_PLAYLIST: raise Exception('This source is a playlist so it doesn\'t have thumbnail.') - try: - response = requests.get(self.url, cookies={'CONSENT': 'YES+1'}) - response.raise_for_status() - except RequestException as e: - print(f"Error occurred while making a request to YouTube: {e}") - return None + return get_youtube_channel_image_info(self.url) - soup = BeautifulSoup(response.text, "html.parser") - - try: - 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"] - except (KeyError, ValueError, TypeError) as e: - print(f"Error occurred while parsing YouTube JSON: {e}") - return None def directory_exists(self): return (os.path.isdir(self.directory_path) and diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 87807b5..b51ca55 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -233,18 +233,16 @@ def download_source_thumbnail(source_id): 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"]: + avatar, banner = source.get_image_url + log.info(f'Thumbnail URL for source with ID: {source_id} ' + f'Avatar: {avatar} ' + f'Banner: {banner}') + if banner != None: + url = banner + i = get_remote_image(url) + image_file = BytesIO() + i.save(image_file, 'JPEG', quality=85, optimize=True, progressive=True) + file_name = "banner.jpg" # Reset file pointer to the beginning for the next save image_file.seek(0) # Create a Django ContentFile from BytesIO stream @@ -253,7 +251,22 @@ def download_source_thumbnail(source_id): 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}') + if avatar != None: + url = avatar + i = get_remote_image(url) + 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 for source with ID: {source_id}') @background(schedule=0) diff --git a/tubesync/sync/youtube.py b/tubesync/sync/youtube.py index 4ac6e83..1ef1fab 100644 --- a/tubesync/sync/youtube.py +++ b/tubesync/sync/youtube.py @@ -35,6 +35,35 @@ def get_yt_opts(): opts.update({'cookiefile': cookie_file_path}) return opts +def get_channel_image_info(url): + opts = get_yt_opts() + opts.update({ + 'skip_download': True, + 'forcejson': True, + 'simulate': True, + 'logger': log, + 'extract_flat': True, # Change to False to get detailed info + }) + + with yt_dlp.YoutubeDL(opts) as y: + try: + response = y.extract_info(url, download=False) + + avatar_url = None + banner_url = None + for thumbnail in response['thumbnails']: + if thumbnail['id'] == 'avatar_uncropped': + avatar_url = thumbnail['url'] + if thumbnail['id'] == 'banner_uncropped': + banner_url = thumbnail['url'] + if banner_url != None and avatar_url != None: + break + + return avatar_url, banner_url + except yt_dlp.utils.DownloadError as e: + raise YouTubeError(f'Failed to extract channel info for "{url}": {e}') from e + + def get_media_info(url): ''' From a359c989d27fbdd3d1090047693b0e71743fec3b Mon Sep 17 00:00:00 2001 From: administrator <7dn1yh5j@debauchez.fr> Date: Mon, 11 Dec 2023 18:58:13 +0100 Subject: [PATCH 04/10] Remove unecessary code --- tubesync/sync/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 9efe026..f0b88a1 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -2,7 +2,6 @@ import os import uuid import json import re -import requests from xml.etree import ElementTree from collections import OrderedDict from datetime import datetime, timedelta From 0478cb399e7a2fa73e6278e8e6fd58d02c7cc610 Mon Sep 17 00:00:00 2001 From: administrator <7dn1yh5j@debauchez.fr> Date: Mon, 11 Dec 2023 19:24:44 +0100 Subject: [PATCH 05/10] Clean code and rename thumbnail by images It now exports banner as banner.jpg and avatar as poster.jpg and season-poster.jpg --- Pipfile | 2 +- ...el_thumbnails2.py => 0021_source_copy_channel_images.py} | 4 ++-- tubesync/sync/models.py | 6 +++--- tubesync/sync/signals.py | 6 +++--- tubesync/sync/tasks.py | 4 ++-- tubesync/sync/views.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) rename tubesync/sync/migrations/{0021_source_copy_channel_thumbnails2.py => 0021_source_copy_channel_images.py} (63%) diff --git a/Pipfile b/Pipfile index bfe7a90..bb2f955 100644 --- a/Pipfile +++ b/Pipfile @@ -22,4 +22,4 @@ mysqlclient = "*" yt-dlp = "*" redis = "*" hiredis = "*" -requests = {extras = ["socks"], version = "*"} \ No newline at end of file +requests = {extras = ["socks"], version = "*"} diff --git a/tubesync/sync/migrations/0021_source_copy_channel_thumbnails2.py b/tubesync/sync/migrations/0021_source_copy_channel_images.py similarity index 63% rename from tubesync/sync/migrations/0021_source_copy_channel_thumbnails2.py rename to tubesync/sync/migrations/0021_source_copy_channel_images.py index 178ac97..a8340b7 100644 --- a/tubesync/sync/migrations/0021_source_copy_channel_thumbnails2.py +++ b/tubesync/sync/migrations/0021_source_copy_channel_images.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): 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'), + name='copy_channel_images', + field=models.BooleanField(default=False, help_text='Copy channel images : banner as banner.jpg and avatar as poster.jpg and season-poster.jpg. These may be detected and used by some media servers', verbose_name='copy channel images'), ), ] diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index f0b88a1..16af753 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -343,10 +343,10 @@ 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'), + copy_channel_images = models.BooleanField( + _('copy channel images'), default=False, - help_text=_('Copy channel thumbnails in poster.jpg and season-poster.jpg, these may be detected and used by some media servers') + help_text=_('Copy channel images : banner as banner.jpg and avatar as poster.jpg and season-poster.jpg. These may be detected and used by some media servers') ) copy_thumbnails = models.BooleanField( _('copy thumbnails'), diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py index abb212c..6d2f182 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_source_thumbnail) + download_media, rescan_media_server, download_source_images) from .utils import delete_file @@ -47,8 +47,8 @@ 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( + if instance.source_type != Source.SOURCE_TYPE_YOUTUBE_PLAYLIST and instance.copy_channel_images: + download_source_images( str(instance.pk), priority=0, verbose_name=verbose_name.format(instance.name) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index b51ca55..0edb0c2 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -221,7 +221,7 @@ def check_source_directory_exists(source_id): @background(schedule=0) -def download_source_thumbnail(source_id): +def download_source_images(source_id): ''' Downloads an image and save it as a local thumbnail attached to a Source instance. @@ -230,7 +230,7 @@ def download_source_thumbnail(source_id): 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 ' + log.error(f'Task download_source_images(pk={source_id}) called but no ' f'source exists with ID: {source_id}') return avatar, banner = source.get_image_url diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index af0ded5..a97b1ea 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -297,7 +297,7 @@ 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_channel_thumbnails', + 'source_acodec', 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_channel_images', 'copy_thumbnails', 'write_nfo', 'write_json', 'embed_metadata', 'embed_thumbnail', 'enable_sponsorblock', 'sponsorblock_categories', 'write_subtitles', 'auto_subtitles', 'sub_langs') From 32a884365becd14dd98f70a914f5c218af4e49d8 Mon Sep 17 00:00:00 2001 From: administrator <7dn1yh5j@debauchez.fr> Date: Mon, 11 Dec 2023 19:38:36 +0100 Subject: [PATCH 06/10] Duplicate banner.jpg to background.jpg --- .../0021_source_copy_channel_images.py | 2 +- tubesync/sync/models.py | 2 +- tubesync/sync/tasks.py | 17 +++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tubesync/sync/migrations/0021_source_copy_channel_images.py b/tubesync/sync/migrations/0021_source_copy_channel_images.py index a8340b7..5d56892 100644 --- a/tubesync/sync/migrations/0021_source_copy_channel_images.py +++ b/tubesync/sync/migrations/0021_source_copy_channel_images.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='source', name='copy_channel_images', - field=models.BooleanField(default=False, help_text='Copy channel images : banner as banner.jpg and avatar as poster.jpg and season-poster.jpg. These may be detected and used by some media servers', verbose_name='copy channel images'), + field=models.BooleanField(default=False, help_text='Copy channel banner and avatar. These may be detected and used by some media servers', verbose_name='copy channel images'), ), ] diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 16af753..ce6d9dc 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -346,7 +346,7 @@ class Source(models.Model): copy_channel_images = models.BooleanField( _('copy channel images'), default=False, - help_text=_('Copy channel images : banner as banner.jpg and avatar as poster.jpg and season-poster.jpg. These may be detected and used by some media servers') + help_text=_('Copy channel banner and avatar. These may be detected and used by some media servers') ) copy_thumbnails = models.BooleanField( _('copy thumbnails'), diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 0edb0c2..cd16326 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -242,14 +242,15 @@ def download_source_images(source_id): i = get_remote_image(url) image_file = BytesIO() i.save(image_file, 'JPEG', quality=85, optimize=True, progressive=True) - file_name = "banner.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()) + + for file_name in ["banner.jpg", "background.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()) if avatar != None: url = avatar From 12b9dfbada992e0088a58a5ce475201971bf3008 Mon Sep 17 00:00:00 2001 From: administrator <7dn1yh5j@debauchez.fr> Date: Mon, 11 Dec 2023 19:49:06 +0100 Subject: [PATCH 07/10] Add Exception when images are requested from a source of a type that does not have any --- tubesync/common/errors.py | 7 +++++++ tubesync/sync/models.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tubesync/common/errors.py b/tubesync/common/errors.py index 130510a..2373da7 100644 --- a/tubesync/common/errors.py +++ b/tubesync/common/errors.py @@ -27,3 +27,10 @@ class DatabaseConnectionError(Exception): Raised when parsing or initially connecting to a database. ''' pass + + +class NoImageSourceException(Exception): + ''' + Raised when images are requested from a source of a type that does not have any. + ''' + pass diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index ce6d9dc..dceb38b 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -491,7 +491,7 @@ class Source(models.Model): @property def get_image_url(self): if self.source_type == self.SOURCE_TYPE_YOUTUBE_PLAYLIST: - raise Exception('This source is a playlist so it doesn\'t have thumbnail.') + raise NoImageSourceException('This source is a playlist so it doesn\'t have thumbnail.') return get_youtube_channel_image_info(self.url) From bb53e78a8f8ca451ef1a0edf2280cff6831859ae Mon Sep 17 00:00:00 2001 From: administrator <7dn1yh5j@debauchez.fr> Date: Mon, 11 Dec 2023 19:50:12 +0100 Subject: [PATCH 08/10] Add missing import --- tubesync/sync/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index dceb38b..b61b901 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -13,7 +13,7 @@ from django.core.validators import RegexValidator from django.utils.text import slugify from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from common.errors import NoFormatException +from common.errors import NoFormatException, NoImageSourceException from common.utils import clean_filename from .youtube import (get_media_info as get_youtube_media_info, download_media as download_youtube_media, From 062cfe8e8694ad88c9c651e4e04563ced10cd2a0 Mon Sep 17 00:00:00 2001 From: Someone <10882916+InterN0te@users.noreply.github.com> Date: Tue, 12 Dec 2023 12:17:18 +0000 Subject: [PATCH 09/10] Use SuspiciousOperation Exception --- tubesync/common/errors.py | 9 +-------- tubesync/sync/models.py | 5 +++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/tubesync/common/errors.py b/tubesync/common/errors.py index 2373da7..4e496e6 100644 --- a/tubesync/common/errors.py +++ b/tubesync/common/errors.py @@ -26,11 +26,4 @@ class DatabaseConnectionError(Exception): ''' Raised when parsing or initially connecting to a database. ''' - pass - - -class NoImageSourceException(Exception): - ''' - Raised when images are requested from a source of a type that does not have any. - ''' - pass + pass \ No newline at end of file diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index b61b901..55139dd 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -8,12 +8,13 @@ from datetime import datetime, timedelta from pathlib import Path from django.conf import settings from django.db import models +from django.core.exceptions import SuspiciousOperation from django.core.files.storage import FileSystemStorage from django.core.validators import RegexValidator from django.utils.text import slugify from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from common.errors import NoFormatException, NoImageSourceException +from common.errors import NoFormatException from common.utils import clean_filename from .youtube import (get_media_info as get_youtube_media_info, download_media as download_youtube_media, @@ -491,7 +492,7 @@ class Source(models.Model): @property def get_image_url(self): if self.source_type == self.SOURCE_TYPE_YOUTUBE_PLAYLIST: - raise NoImageSourceException('This source is a playlist so it doesn\'t have thumbnail.') + raise SuspiciousOperation('This source is a playlist so it doesn\'t have thumbnail.') return get_youtube_channel_image_info(self.url) From 08e12507f415765027fb2aa4819834ce121386c4 Mon Sep 17 00:00:00 2001 From: Someone <10882916+InterN0te@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:18:32 +0100 Subject: [PATCH 10/10] Restore common.errors.py --- tubesync/common/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/common/errors.py b/tubesync/common/errors.py index 4e496e6..130510a 100644 --- a/tubesync/common/errors.py +++ b/tubesync/common/errors.py @@ -26,4 +26,4 @@ class DatabaseConnectionError(Exception): ''' Raised when parsing or initially connecting to a database. ''' - pass \ No newline at end of file + pass