Add option to export channel thumbnails for Jellyfin

pull/449/head
administrator 2023-12-10 19:06:00 +01:00
parent 3a87b5779e
commit e9d0599569
6 changed files with 81 additions and 3 deletions

View File

@ -23,3 +23,4 @@ yt-dlp = "*"
redis = "*"
hiredis = "*"
requests = {extras = ["socks"], version = "*"}
bs4 = "*"

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

View File

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

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_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}')

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

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