ffmpeg embed thumbnails, configuration
This commit is contained in:
parent
1d5579aa31
commit
c927f32aa6
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, Optional, Dict
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
@ -7,7 +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.forms import MultipleChoiceField, CheckboxSelectMultiple
|
from django.forms import MultipleChoiceField, CheckboxSelectMultiple, Field, TypedMultipleChoiceField
|
||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -24,37 +25,44 @@ from .mediaservers import PlexMediaServer
|
||||||
|
|
||||||
media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/')
|
media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/')
|
||||||
|
|
||||||
class CommaSepField(models.Field):
|
|
||||||
"Implements comma-separated storage of lists"
|
|
||||||
|
|
||||||
def __init__(self, separator=",", *args, **kwargs):
|
|
||||||
self.separator = separator
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def deconstruct(self):
|
|
||||||
name, path, args, kwargs = super().deconstruct()
|
|
||||||
# Only include kwarg if it's not the default
|
|
||||||
if self.separator != ",":
|
|
||||||
kwargs['separator'] = self.separator
|
|
||||||
return name, path, args, kwargs
|
|
||||||
|
|
||||||
|
|
||||||
class CustomCheckboxSelectMultiple(CheckboxSelectMultiple):
|
class CustomCheckboxSelectMultiple(CheckboxSelectMultiple):
|
||||||
template_name = 'widgets/checkbox_select.html'
|
template_name = 'widgets/checkbox_select.html'
|
||||||
option_template_name = 'widgets/checkbox_option.html'
|
option_template_name = 'widgets/checkbox_option.html'
|
||||||
|
|
||||||
class CommaSepChoiceField(CommaSepField):
|
def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]:
|
||||||
|
ctx = super().get_context(name, value, attrs)['widget']
|
||||||
|
ctx["multipleChoiceProperties"] = []
|
||||||
|
for _group, options, _index in ctx["optgroups"]:
|
||||||
|
for option in options:
|
||||||
|
if not isinstance(value,list) and ( option["value"] in value.selected_choices or ( value.allow_all and value.all_choice in value.selected_choices ) ):
|
||||||
|
checked = True
|
||||||
|
else:
|
||||||
|
checked = False
|
||||||
|
|
||||||
|
ctx["multipleChoiceProperties"].append({
|
||||||
|
"template_name": option["template_name"],
|
||||||
|
"type": option["type"],
|
||||||
|
"value": option["value"],
|
||||||
|
"label": option["label"],
|
||||||
|
"name": option["name"],
|
||||||
|
"checked": checked})
|
||||||
|
|
||||||
|
return { 'widget': ctx }
|
||||||
|
|
||||||
|
class CommaSepChoiceField(models.Field):
|
||||||
"Implements comma-separated storage of lists"
|
"Implements comma-separated storage of lists"
|
||||||
|
|
||||||
def __init__(self, separator=",", possible_choices=(("","")), *args, **kwargs):
|
def __init__(self, separator=",", possible_choices=(("","")), all_choice="", all_label="All", allow_all=False, *args, **kwargs):
|
||||||
print(">",separator, possible_choices, args, kwargs)
|
self.separator = separator
|
||||||
self.possible_choices = possible_choices
|
self.possible_choices = possible_choices
|
||||||
super().__init__(separator=separator, *args, **kwargs)
|
self.selected_choices = []
|
||||||
|
self.allow_all = allow_all
|
||||||
|
self.all_label = all_label
|
||||||
|
self.all_choice = all_choice
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def deconstruct(self):
|
def deconstruct(self):
|
||||||
name, path, args, kwargs = super().deconstruct()
|
name, path, args, kwargs = super().deconstruct()
|
||||||
print("<",name,path,args,kwargs)
|
|
||||||
# Only include kwarg if it's not the default
|
|
||||||
if self.separator != ",":
|
if self.separator != ",":
|
||||||
kwargs['separator'] = self.separator
|
kwargs['separator'] = self.separator
|
||||||
kwargs['possible_choices'] = self.possible_choices
|
kwargs['possible_choices'] = self.possible_choices
|
||||||
|
@ -63,25 +71,54 @@ class CommaSepChoiceField(CommaSepField):
|
||||||
def db_type(self, _connection):
|
def db_type(self, _connection):
|
||||||
return 'char(1024)'
|
return 'char(1024)'
|
||||||
|
|
||||||
def get_choices(self):
|
def get_my_choices(self):
|
||||||
choiceArray = []
|
choiceArray = []
|
||||||
if self.possible_choices is None:
|
if self.possible_choices is None:
|
||||||
return choiceArray
|
return choiceArray
|
||||||
|
if self.allow_all:
|
||||||
|
choiceArray.append((self.all_choice, _(self.all_label)))
|
||||||
|
|
||||||
for t in self.possible_choices:
|
for t in self.possible_choices:
|
||||||
choiceArray.append(t)
|
choiceArray.append(t)
|
||||||
|
|
||||||
return choiceArray
|
return choiceArray
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
# This is a fairly standard way to set up some defaults
|
# This is a fairly standard way to set up some defaults
|
||||||
# while letting the caller override them.
|
# while letting the caller override them.
|
||||||
print(self.choices)
|
|
||||||
defaults = {'form_class': MultipleChoiceField,
|
defaults = {'form_class': MultipleChoiceField,
|
||||||
'choices': self.get_choices,
|
'choices': self.get_my_choices,
|
||||||
'widget': CustomCheckboxSelectMultiple}
|
'widget': CustomCheckboxSelectMultiple,
|
||||||
|
'label': '',
|
||||||
|
'required': False}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
#del defaults.required
|
#del defaults.required
|
||||||
return super().formfield(**defaults)
|
return super().formfield(**defaults)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
# Only include kwarg if it's not the default
|
||||||
|
if self.separator != ",":
|
||||||
|
kwargs['separator'] = self.separator
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
def from_db_value(self, value, expr, conn):
|
||||||
|
if value is None:
|
||||||
|
self.selected_choices = []
|
||||||
|
else:
|
||||||
|
self.selected_choices = value.split(",")
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if not isinstance(value,list):
|
||||||
|
print("?! CommaSepChoiceField -> ",value)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return ",".join(value)
|
||||||
|
|
||||||
class Source(models.Model):
|
class Source(models.Model):
|
||||||
'''
|
'''
|
||||||
A Source is a source of media. Currently, this is either a YouTube channel
|
A Source is a source of media. Currently, this is either a YouTube channel
|
||||||
|
@ -167,7 +204,6 @@ class Source(models.Model):
|
||||||
|
|
||||||
# as stolen from: https://wiki.sponsor.ajay.app/w/Types / https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/sponsorblock.py
|
# as stolen from: https://wiki.sponsor.ajay.app/w/Types / https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/sponsorblock.py
|
||||||
SPONSORBLOCK_CATEGORIES_CHOICES = (
|
SPONSORBLOCK_CATEGORIES_CHOICES = (
|
||||||
('all', 'All'),
|
|
||||||
('sponsor', 'Sponsor'),
|
('sponsor', 'Sponsor'),
|
||||||
('intro', 'Intermission/Intro Animation'),
|
('intro', 'Intermission/Intro Animation'),
|
||||||
('outro', 'Endcards/Credits'),
|
('outro', 'Endcards/Credits'),
|
||||||
|
@ -179,8 +215,13 @@ class Source(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
sponsorblock_categories = CommaSepChoiceField(
|
sponsorblock_categories = CommaSepChoiceField(
|
||||||
|
_(''),
|
||||||
possible_choices=SPONSORBLOCK_CATEGORIES_CHOICES,
|
possible_choices=SPONSORBLOCK_CATEGORIES_CHOICES,
|
||||||
default="all"
|
all_choice="all",
|
||||||
|
allow_all=True,
|
||||||
|
all_label="(all options)",
|
||||||
|
default="all",
|
||||||
|
help_text=_("Select the sponsorblocks you want to enforce")
|
||||||
)
|
)
|
||||||
|
|
||||||
embed_metadata = models.BooleanField(
|
embed_metadata = models.BooleanField(
|
||||||
|
@ -1386,7 +1427,9 @@ class Media(models.Model):
|
||||||
f'no valid format available')
|
f'no valid format available')
|
||||||
# Download the media with youtube-dl
|
# Download the media with youtube-dl
|
||||||
download_youtube_media(self.url, format_str, self.source.extension,
|
download_youtube_media(self.url, format_str, self.source.extension,
|
||||||
str(self.filepath), self.source.write_json)
|
str(self.filepath), self.source.write_json,
|
||||||
|
self.source.sponsorblock_categories, self.source.embed_thumbnail,
|
||||||
|
self.source.embed_metadata, self.source.enable_sponsorblock)
|
||||||
# Return the download paramaters
|
# Return the download paramaters
|
||||||
return format_str, self.source.extension
|
return format_str, self.source.extension
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
<label for="{{ option.value }}">{{option.label}}</label>-->
|
<label for="{{ option.value }}">{{option.label}}</label>-->
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<input type="{{ option.type }}" name="{{ option.name }}" value="{{ option.value }}" id="{{ option.value }}">
|
<input type="{{ option.type }}" name="{{ option.name }}" value="{{ option.value }}" id="{{ option.value }}" {% if option.checked %}checked{% endif %}>
|
||||||
<span>{{option.label}}</span>
|
<span>{{option.label}}</span>
|
||||||
</label>
|
</label>
|
|
@ -1,5 +1,5 @@
|
||||||
{% for group, options, index in widget.optgroups %}
|
</label>
|
||||||
{% for option in options %}
|
{% for option in widget.multipleChoiceProperties %}
|
||||||
{% include option.template_name with option=option %}
|
{% include option.template_name with option=option %}
|
||||||
{% endfor%}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<label>
|
|
@ -64,7 +64,9 @@ def get_media_info(url):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def download_media(url, media_format, extension, output_file, info_json, sponsor_categories="all", embed_thumbnail=False, embed_metadata=False, skip_sponsors=True):
|
def download_media(url, media_format, extension, output_file, info_json,
|
||||||
|
sponsor_categories="all",
|
||||||
|
embed_thumbnail=False, embed_metadata=False, skip_sponsors=True):
|
||||||
'''
|
'''
|
||||||
Downloads a YouTube URL to a file on disk.
|
Downloads a YouTube URL to a file on disk.
|
||||||
'''
|
'''
|
||||||
|
@ -100,12 +102,6 @@ def download_media(url, media_format, extension, output_file, info_json, sponsor
|
||||||
else:
|
else:
|
||||||
log.warn(f'[youtube-dl] unknown event: {str(event)}')
|
log.warn(f'[youtube-dl] unknown event: {str(event)}')
|
||||||
hook.download_progress = 0
|
hook.download_progress = 0
|
||||||
postprocessors = []
|
|
||||||
postprocessors.append({
|
|
||||||
'key': 'FFmpegMetadata',
|
|
||||||
'add_chapters': True,
|
|
||||||
'add_metadata': True
|
|
||||||
})
|
|
||||||
# Pending configuration options from PR #338
|
# Pending configuration options from PR #338
|
||||||
#postprocessors.append({
|
#postprocessors.append({
|
||||||
# 'key': 'SponsorBlock',
|
# 'key': 'SponsorBlock',
|
||||||
|
@ -119,8 +115,30 @@ def download_media(url, media_format, extension, output_file, info_json, sponsor
|
||||||
'quiet': True,
|
'quiet': True,
|
||||||
'progress_hooks': [hook],
|
'progress_hooks': [hook],
|
||||||
'writeinfojson': info_json,
|
'writeinfojson': info_json,
|
||||||
'postprocessors': postprocessors,
|
'postprocessors': []
|
||||||
})
|
}
|
||||||
|
sbopt = {
|
||||||
|
'key': 'SponsorBlock',
|
||||||
|
'categories': [sponsor_categories]
|
||||||
|
}
|
||||||
|
ffmdopt = {
|
||||||
|
'key': 'FFmpegMetadata',
|
||||||
|
'add_chapters': True,
|
||||||
|
'add_metadata': True
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = get_yt_opts()
|
||||||
|
if embed_thumbnail:
|
||||||
|
ytopts['postprocessors'].append({'key': 'EmbedThumbnail'})
|
||||||
|
if embed_metadata:
|
||||||
|
ffmdopt["add_metadata"] = True
|
||||||
|
if skip_sponsors:
|
||||||
|
ytopts['postprocessors'].append(sbopt)
|
||||||
|
|
||||||
|
ytopts['postprocessors'].append(ffmdopt)
|
||||||
|
|
||||||
|
opts.update(ytopts)
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(opts) as y:
|
with yt_dlp.YoutubeDL(opts) as y:
|
||||||
try:
|
try:
|
||||||
return y.download([url])
|
return y.download([url])
|
||||||
|
|
Loading…
Reference in New Issue