Merge 08e12507f4
into b11b667aff
This commit is contained in:
commit
4c7c420b0a
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,6 +8,7 @@ from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.utils.text import slugify
|
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.errors import NoFormatException
|
||||||
from common.utils import clean_filename
|
from common.utils import clean_filename
|
||||||
from .youtube import (get_media_info as get_youtube_media_info,
|
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 .utils import seconds_to_timestr, parse_media_format
|
||||||
from .matching import (get_best_combined_format, get_best_audio_format,
|
from .matching import (get_best_combined_format, get_best_audio_format,
|
||||||
get_best_video_format)
|
get_best_video_format)
|
||||||
|
@ -338,6 +340,11 @@ class Source(models.Model):
|
||||||
default=FALLBACK_NEXT_BEST_HD,
|
default=FALLBACK_NEXT_BEST_HD,
|
||||||
help_text=_('What do do when media in your source resolution and codecs is not available')
|
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 = models.BooleanField(
|
||||||
_('copy thumbnails'),
|
_('copy thumbnails'),
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -478,6 +485,14 @@ class Source(models.Model):
|
||||||
def make_directory(self):
|
def make_directory(self):
|
||||||
return os.makedirs(self.directory_path, exist_ok=True)
|
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):
|
def directory_exists(self):
|
||||||
return (os.path.isdir(self.directory_path) and
|
return (os.path.isdir(self.directory_path) and
|
||||||
os.access(self.directory_path, os.W_OK))
|
os.access(self.directory_path, os.W_OK))
|
||||||
|
|
|
@ -10,7 +10,7 @@ from .models import Source, Media, MediaServer
|
||||||
from .tasks import (delete_task_by_source, delete_task_by_media, index_source_task,
|
from .tasks import (delete_task_by_source, delete_task_by_media, index_source_task,
|
||||||
download_media_thumbnail, download_media_metadata,
|
download_media_thumbnail, download_media_metadata,
|
||||||
map_task_to_instance, check_source_directory_exists,
|
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
|
from .utils import delete_file
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,6 +47,12 @@ def source_post_save(sender, instance, created, **kwargs):
|
||||||
priority=0,
|
priority=0,
|
||||||
verbose_name=verbose_name.format(instance.name)
|
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:
|
if instance.index_schedule > 0:
|
||||||
delete_task_by_source('sync.tasks.index_source_task', instance.pk)
|
delete_task_by_source('sync.tasks.index_source_task', instance.pk)
|
||||||
log.info(f'Scheduling media indexing for source: {instance.name}')
|
log.info(f'Scheduling media indexing for source: {instance.name}')
|
||||||
|
|
|
@ -14,6 +14,7 @@ from datetime import timedelta, datetime
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
@ -219,6 +220,56 @@ def check_source_directory_exists(source_id):
|
||||||
source.make_directory()
|
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)
|
@background(schedule=0)
|
||||||
def download_media_metadata(media_id):
|
def download_media_metadata(media_id):
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -297,8 +297,8 @@ class EditSourceMixin:
|
||||||
fields = ('source_type', 'key', 'name', 'directory', 'filter_text', 'media_format',
|
fields = ('source_type', 'key', 'name', 'directory', 'filter_text', 'media_format',
|
||||||
'index_schedule', 'download_media', 'download_cap', 'delete_old_media',
|
'index_schedule', 'download_media', 'download_cap', 'delete_old_media',
|
||||||
'delete_removed_media', 'days_to_keep', 'source_resolution', 'source_vcodec',
|
'delete_removed_media', 'days_to_keep', 'source_resolution', 'source_vcodec',
|
||||||
'source_acodec', 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_thumbnails',
|
'source_acodec', 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_channel_images',
|
||||||
'write_nfo', 'write_json', 'embed_metadata', 'embed_thumbnail',
|
'copy_thumbnails', 'write_nfo', 'write_json', 'embed_metadata', 'embed_thumbnail',
|
||||||
'enable_sponsorblock', 'sponsorblock_categories', 'write_subtitles',
|
'enable_sponsorblock', 'sponsorblock_categories', 'write_subtitles',
|
||||||
'auto_subtitles', 'sub_langs')
|
'auto_subtitles', 'sub_langs')
|
||||||
errors = {
|
errors = {
|
||||||
|
|
|
@ -35,6 +35,35 @@ def get_yt_opts():
|
||||||
opts.update({'cookiefile': cookie_file_path})
|
opts.update({'cookiefile': cookie_file_path})
|
||||||
return opts
|
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):
|
def get_media_info(url):
|
||||||
'''
|
'''
|
||||||
|
|
Loading…
Reference in New Issue