diff --git a/.gitignore b/.gitignore index e6e892e..eec472f 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,5 @@ dmypy.json # Pyre type checker .pyre/ -Pipfile.lock \ No newline at end of file +Pipfile.lock +.vscode/launch.json diff --git a/Pipfile b/Pipfile index 243f0f3..21e4e49 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] +autopep8 = "*" [packages] django = "~=3.2" diff --git a/tubesync/sync/migrations/0015_auto_20230214_2052.py b/tubesync/sync/migrations/0015_auto_20230214_2052.py new file mode 100644 index 0000000..aab006f --- /dev/null +++ b/tubesync/sync/migrations/0015_auto_20230214_2052.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.18 on 2023-02-14 20:52 + +from django.db import migrations, models +import sync.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sync', '0014_alter_media_media_file'), + ] + + operations = [ + migrations.AddField( + model_name='source', + name='embed_metadata', + field=models.BooleanField(default=False, help_text='Embed metadata from source into file', verbose_name='embed metadata'), + ), + migrations.AddField( + model_name='source', + name='embed_thumbnail', + field=models.BooleanField(default=False, help_text='Embed thumbnail into the file', verbose_name='embed thumbnail'), + ), + migrations.AddField( + model_name='source', + name='enable_sponsorblock', + field=models.BooleanField(default=True, help_text='Use SponsorBlock?', verbose_name='enable sponsorblock'), + ), + migrations.AddField( + model_name='source', + name='sponsorblock_categories', + field=sync.models.CommaSepChoiceField(default='all', possible_choices=(('all', 'All'), ('sponsor', 'Sponsor'), ('intro', 'Intermission/Intro Animation'), ('outro', 'Endcards/Credits'), ('selfpromo', 'Unpaid/Self Promotion'), ('preview', 'Preview/Recap'), ('filler', 'Filler Tangent'), ('interaction', 'Interaction Reminder'), ('music_offtopic', 'Non-Music Section'))), + ), + ] diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 16415c7..9279c1f 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from pathlib import Path from django.conf import settings from django.db import models +from django.forms import MultipleChoiceField, CheckboxSelectMultiple from django.core.files.storage import FileSystemStorage from django.utils.text import slugify from django.utils import timezone @@ -23,6 +24,63 @@ from .mediaservers import PlexMediaServer 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): + template_name = 'widgets/checkbox_select.html' + option_template_name = 'widgets/checkbox_option.html' + +class CommaSepChoiceField(CommaSepField): + "Implements comma-separated storage of lists" + + def __init__(self, separator=",", possible_choices=(("","")), *args, **kwargs): + print(">",separator, possible_choices, args, kwargs) + self.possible_choices = possible_choices + super().__init__(separator=separator, *args, **kwargs) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + print("<",name,path,args,kwargs) + # Only include kwarg if it's not the default + if self.separator != ",": + kwargs['separator'] = self.separator + kwargs['possible_choices'] = self.possible_choices + return name, path, args, kwargs + + def db_type(self, _connection): + return 'char(1024)' + + def get_choices(self): + choiceArray = [] + if self.possible_choices is None: + return choiceArray + for t in self.possible_choices: + choiceArray.append(t) + return choiceArray + + def formfield(self, **kwargs): + # This is a fairly standard way to set up some defaults + # while letting the caller override them. + print(self.choices) + defaults = {'form_class': MultipleChoiceField, + 'choices': self.get_choices, + 'widget': CustomCheckboxSelectMultiple} + defaults.update(kwargs) + #del defaults.required + return super().formfield(**defaults) class Source(models.Model): ''' @@ -106,6 +164,43 @@ class Source(models.Model): EXTENSION_MKV = 'mkv' EXTENSIONS = (EXTENSION_M4A, EXTENSION_OGG, EXTENSION_MKV) + + # 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 = ( + ('all', 'All'), + ('sponsor', 'Sponsor'), + ('intro', 'Intermission/Intro Animation'), + ('outro', 'Endcards/Credits'), + ('selfpromo', 'Unpaid/Self Promotion'), + ('preview', 'Preview/Recap'), + ('filler', 'Filler Tangent'), + ('interaction', 'Interaction Reminder'), + ('music_offtopic', 'Non-Music Section'), + ) + + sponsorblock_categories = CommaSepChoiceField( + possible_choices=SPONSORBLOCK_CATEGORIES_CHOICES, + default="all" + ) + + embed_metadata = models.BooleanField( + _('embed metadata'), + default=False, + help_text=_('Embed metadata from source into file') + ) + embed_thumbnail = models.BooleanField( + _('embed thumbnail'), + default=False, + help_text=_('Embed thumbnail into the file') + ) + + enable_sponsorblock = models.BooleanField( + _('enable sponsorblock'), + default=True, + help_text=_('Use SponsorBlock?') + ) + + # Fontawesome icons used for the source on the front end ICONS = { SOURCE_TYPE_YOUTUBE_CHANNEL: '', diff --git a/tubesync/sync/templates/widgets/checkbox_option.html b/tubesync/sync/templates/widgets/checkbox_option.html new file mode 100644 index 0000000..8578ecc --- /dev/null +++ b/tubesync/sync/templates/widgets/checkbox_option.html @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/tubesync/sync/templates/widgets/checkbox_select.html b/tubesync/sync/templates/widgets/checkbox_select.html new file mode 100644 index 0000000..f621908 --- /dev/null +++ b/tubesync/sync/templates/widgets/checkbox_select.html @@ -0,0 +1,5 @@ +{% for group, options, index in widget.optgroups %} + {% for option in options %} + {% include option.template_name with option=option %} + {% endfor%} +{% endfor %} \ No newline at end of file diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index 912dd22..5739ee0 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -297,7 +297,9 @@ class EditSourceMixin: fields = ('source_type', 'key', 'name', 'directory', 'media_format', 'index_schedule', 'download_media', 'download_cap', 'delete_old_media', 'days_to_keep', 'source_resolution', 'source_vcodec', 'source_acodec', - 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_thumbnails', 'write_nfo', 'write_json') + 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_thumbnails', 'write_nfo', + 'write_json', 'embed_metadata', 'embed_thumbnail', 'enable_sponsorblock', + 'sponsorblock_categories') errors = { 'invalid_media_format': _('Invalid media format, the media format contains ' 'errors or is empty. Check the table at the end of ' diff --git a/tubesync/sync/youtube.py b/tubesync/sync/youtube.py index 08b60fe..9bbfbb7 100644 --- a/tubesync/sync/youtube.py +++ b/tubesync/sync/youtube.py @@ -64,7 +64,7 @@ def get_media_info(url): return response -def download_media(url, media_format, extension, output_file, info_json, sponsor_categories="all"): +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. ''' @@ -101,23 +101,37 @@ def download_media(url, media_format, extension, output_file, info_json, sponsor log.warn(f'[youtube-dl] unknown event: {str(event)}') hook.download_progress = 0 - opts = get_yt_opts() - opts.update({ + ytopts = { 'format': media_format, 'merge_output_format': extension, 'outtmpl': output_file, 'quiet': True, 'progress_hooks': [hook], 'writeinfojson': info_json, - 'postprocessors': [{ + 'postprocessors': [] + } + sbopt = { 'key': 'SponsorBlock', 'categories': [sponsor_categories] - },{ - 'key': 'FFmpegMetadata', + } + ffmdopt = { + 'key': 'FFmpegMetadata', 'add_chapters': True, 'add_metadata': True - }] - }) + } + + opts = get_yt_opts() + if embed_thumbnail: + ytopts['postprocessors'].push({'key': 'EmbedThumbnail'}) + if embed_metadata: + ffmdopt["embed-metadata"] = True + if skip_sponsors: + ytopts['postprocessors'].push(sbopt) + + ytopts['postprocessors'].push(ffmdopt) + + opts.update(ytopts) + with yt_dlp.YoutubeDL(opts) as y: try: return y.download([url])