This commit is contained in:
Someone 2024-01-03 03:05:52 -05:00 committed by GitHub
commit 4c7c420b0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 123 additions and 4 deletions

View File

@ -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_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'),
),
]

View File

@ -8,6 +8,7 @@ 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
@ -16,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)
@ -338,6 +340,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_images = models.BooleanField(
_('copy channel images'),
default=False,
help_text=_('Copy channel banner and avatar. These may be detected and used by some media servers')
)
copy_thumbnails = models.BooleanField(
_('copy thumbnails'),
default=False,
@ -478,6 +485,14 @@ class Source(models.Model):
def make_directory(self):
return os.makedirs(self.directory_path, exist_ok=True)
@property
def get_image_url(self):
if self.source_type == self.SOURCE_TYPE_YOUTUBE_PLAYLIST:
raise SuspiciousOperation('This source is a playlist so it doesn\'t have thumbnail.')
return get_youtube_channel_image_info(self.url)
def directory_exists(self):
return (os.path.isdir(self.directory_path) and
os.access(self.directory_path, os.W_OK))

View File

@ -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_images)
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_images:
download_source_images(
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}')

View File

@ -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,56 @@ def check_source_directory_exists(source_id):
source.make_directory()
@background(schedule=0)
def download_source_images(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_images(pk={source_id}) called but no '
f'source exists with ID: {source_id}')
return
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)
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
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)
def download_media_metadata(media_id):
'''

View File

@ -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_images',
'copy_thumbnails', 'write_nfo', 'write_json', 'embed_metadata', 'embed_thumbnail',
'enable_sponsorblock', 'sponsorblock_categories', 'write_subtitles',
'auto_subtitles', 'sub_langs')
errors = {

View File

@ -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):
'''