From 106da4616feecf514b382acf04f155253a555dca Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 18 Jul 2025 05:35:34 -0400 Subject: [PATCH 1/7] Store the metadata from channels or playlists --- tubesync/sync/youtube.py | 52 +++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/tubesync/sync/youtube.py b/tubesync/sync/youtube.py index c0e55f6d..1b7122a5 100644 --- a/tubesync/sync/youtube.py +++ b/tubesync/sync/youtube.py @@ -81,7 +81,10 @@ def get_channel_id(url): else: return channel_id -def get_channel_image_info(url): +def get_image_info(url): + avatar_url = None + banner_url = None + thumbnail_url = None opts = get_yt_opts() opts.update({ 'skip_download': True, @@ -94,20 +97,47 @@ def get_channel_image_info(url): with yt_dlp.YoutubeDL(opts) as y: try: response = y.extract_info(url, download=False) - - avatar_url = None - banner_url = None + except yt_dlp.utils.DownloadError as e: + raise YouTubeError(f'Failed to extract info for "{url}": {e}') from e + else: + height = 0 for thumbnail in response['thumbnails']: if thumbnail['id'] == 'avatar_uncropped': avatar_url = thumbnail['url'] - if thumbnail['id'] == 'banner_uncropped': + elif thumbnail['id'] == 'banner_uncropped': banner_url = thumbnail['url'] - if banner_url is not None and avatar_url is not 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 + elif (thumbnail.get('height') or 0) > height: + thumbnail_url = thumbnail['url'] + try: + entry_type = response['entries'][0].get('_type') + except IndexError: + # an empty entries list + pass + else: + if 'url' == entry_type: + del response['entries'] + elif 'playlist' == entry_type: + for playlist in response['entries']: + del playlist['entries'] + from .models import Metadata + t = Metadata.objects.defer('value').filter( + source__isnull=True, + media__isnull=True, + ).get_or_create( + key=response['id'], + site=response['extractor_key'], + ) + md = t[0] + field_defaults = { + f.attname: f.get_default() + for f in md._meta.fields + if f.has_default() + } + if 'retrieved' in field_defaults: + md.retrieved = field_defaults['retrieved'] + md.value = response + md.save() + return avatar_url, banner_url, thumbnail_url def _subscriber_only(msg='', response=None): From ae75302da4f07dfd8dbd55085510636d6141bba4 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 18 Jul 2025 06:01:56 -0400 Subject: [PATCH 2/7] Update source.py --- tubesync/sync/models/source.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index 42afd2ca..7930a214 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -5,7 +5,6 @@ from collections import deque as queue from pathlib import Path from django import db from django.conf import settings -from django.core.exceptions import SuspiciousOperation from django.core.validators import RegexValidator from django.utils import timezone from django.utils.text import slugify @@ -20,7 +19,7 @@ from ..choices import (Val, from ..fields import CommaSepChoiceField from ..youtube import ( get_media_info as get_youtube_media_info, - get_channel_image_info as get_youtube_channel_image_info, + get_image_info as get_youtube_image_info, ) from ._migrations import media_file_storage from ._private import _srctype_dict @@ -484,10 +483,7 @@ class Source(db.models.Model): @property def get_image_url(self): - if self.is_playlist: - raise SuspiciousOperation('This source is a playlist so it doesn\'t have thumbnail.') - - return get_youtube_channel_image_info(self.url) + return get_youtube_image_info(self.url) def directory_exists(self): From f66809113a3743ee731471a1761ca7fe1cc9dafe Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 18 Jul 2025 06:09:52 -0400 Subject: [PATCH 3/7] Update tasks.py --- tubesync/sync/tasks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 00f38a20..170be1b9 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -720,10 +720,11 @@ def download_source_images(source_id): log.error(f'Task download_source_images(pk={source_id}) called but no ' f'source exists with ID: {source_id}') raise CancelExecution(_('no such source'), retry=False) from e - avatar, banner = source.get_image_url + avatar, banner, thumbnail = source.get_image_url log.info(f'Thumbnail URL for source with ID: {source_id} / {source} ' f'Avatar: {avatar} ' - f'Banner: {banner}') + f'Banner: {banner} ' + f'Thumbnail: {thumbnail}') if banner is not None: url = banner i = get_remote_image(url) From efe0d5a09c9cb5c3dc28536f128ef3c5c755b99c Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 18 Jul 2025 06:20:32 -0400 Subject: [PATCH 4/7] Normalize `height` before comparing to `max_height` --- tubesync/sync/youtube.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/youtube.py b/tubesync/sync/youtube.py index 1b7122a5..7bfda388 100644 --- a/tubesync/sync/youtube.py +++ b/tubesync/sync/youtube.py @@ -100,13 +100,19 @@ def get_image_info(url): except yt_dlp.utils.DownloadError as e: raise YouTubeError(f'Failed to extract info for "{url}": {e}') from e else: - height = 0 + max_height = 0 for thumbnail in response['thumbnails']: + thumbnail_height = thumbnail.get('height') + try: + thumbnail_height = int(thumbnail_height) + except (TypeError, ValueError,): + thumbnail_height = int() if thumbnail['id'] == 'avatar_uncropped': avatar_url = thumbnail['url'] elif thumbnail['id'] == 'banner_uncropped': banner_url = thumbnail['url'] - elif (thumbnail.get('height') or 0) > height: + elif thumbnail_height > max_height: + max_height = thumbnail_height thumbnail_url = thumbnail['url'] try: entry_type = response['entries'][0].get('_type') From 1136f20128738961138ac54a6ffcabad7900fc08 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 18 Jul 2025 06:30:49 -0400 Subject: [PATCH 5/7] Accept playlists also --- tubesync/sync/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py index fdd65981..21dd5afb 100644 --- a/tubesync/sync/signals.py +++ b/tubesync/sync/signals.py @@ -43,7 +43,7 @@ def source_pre_save(sender, instance, **kwargs): check_source_directory_exists.call_local(*args) existing_copy_channel_images = existing_source.copy_channel_images new_copy_channel_images = instance.copy_channel_images - if new_copy_channel_images and not (existing_copy_channel_images or instance.is_playlist): + if new_copy_channel_images and existing_copy_channel_images: download_source_images(str(instance.pk)) existing_dirpath = existing_source.directory_path.resolve(strict=True) new_dirpath = instance.directory_path.resolve(strict=False) @@ -117,7 +117,7 @@ def source_post_save(sender, instance, created, **kwargs): # Check directory exists and create an indexing task for newly created sources if created: check_source_directory_exists(str(source.pk)) - if source.copy_channel_images and not source.is_playlist: + if source.copy_channel_images: download_source_images(str(source.pk)) if source.is_active: log.info(f'Scheduling first media indexing for source: {source.name}') From d619aca56f55ba069ac932f3ce37b7f996632825 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 18 Jul 2025 06:32:38 -0400 Subject: [PATCH 6/7] fixup: restore the `not` --- tubesync/sync/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py index 21dd5afb..13e1084d 100644 --- a/tubesync/sync/signals.py +++ b/tubesync/sync/signals.py @@ -43,7 +43,7 @@ def source_pre_save(sender, instance, **kwargs): check_source_directory_exists.call_local(*args) existing_copy_channel_images = existing_source.copy_channel_images new_copy_channel_images = instance.copy_channel_images - if new_copy_channel_images and existing_copy_channel_images: + if new_copy_channel_images and not existing_copy_channel_images: download_source_images(str(instance.pk)) existing_dirpath = existing_source.directory_path.resolve(strict=True) new_dirpath = instance.directory_path.resolve(strict=False) From d4f3b6addf16327e4c86fc8493a96e2176d6f941 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 18 Jul 2025 07:38:29 -0400 Subject: [PATCH 7/7] Save the `thumbnail` URL to a file --- tubesync/sync/tasks.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 170be1b9..ede85d2e 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -725,6 +725,22 @@ def download_source_images(source_id): f'Avatar: {avatar} ' f'Banner: {banner} ' f'Thumbnail: {thumbnail}') + if thumbnail is not None: + url = thumbnail + i = get_remote_image(url) + image_file = BytesIO() + i.save(image_file, 'JPEG', quality=85, optimize=True, progressive=True) + + for file_name in ["thumbnail.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()) + i = image_file = None + if banner is not None: url = banner i = get_remote_image(url)