diff --git a/app/YtManagerApp/dynamic_preferences_registry.py b/app/YtManagerApp/dynamic_preferences_registry.py index d8a3235..7b1609f 100644 --- a/app/YtManagerApp/dynamic_preferences_registry.py +++ b/app/YtManagerApp/dynamic_preferences_registry.py @@ -12,8 +12,6 @@ import os hidden = Section('hidden') general = Section('general') scheduler = Section('scheduler') -manager = Section('manager') -downloader = Section('downloader') # Hidden settings @@ -60,7 +58,6 @@ class SchedulerConcurrency(IntegerPreference): # User settings @user_preferences_registry.register class MarkDeletedAsWatched(BooleanPreference): - section = manager name = 'mark_deleted_as_watched' default = True required = True @@ -68,47 +65,41 @@ class MarkDeletedAsWatched(BooleanPreference): @user_preferences_registry.register class AutoDeleteWatched(BooleanPreference): - section = manager - name = 'auto_delete_watched' + name = 'automatically_delete_watched' default = True required = True @user_preferences_registry.register class AutoDownloadEnabled(BooleanPreference): - section = downloader - name = 'auto_enabled' + name = 'auto_download' default = True required = True @user_preferences_registry.register class DownloadGlobalLimit(IntegerPreference): - section = downloader - name = 'global_limit' - default = None + name = 'download_global_limit' + default = -1 required = False @user_preferences_registry.register class DownloadGlobalSizeLimit(IntegerPreference): - section = downloader - name = 'global_size_limit_mb' - default = None + name = 'download_global_size_limit' + default = -1 required = False @user_preferences_registry.register class DownloadSubscriptionLimit(IntegerPreference): - section = downloader - name = 'limit_per_subscription' + name = 'download_subscription_limit' default = 5 required = False @user_preferences_registry.register class DownloadMaxAttempts(IntegerPreference): - section = downloader name = 'max_download_attempts' default = 3 required = True @@ -116,7 +107,6 @@ class DownloadMaxAttempts(IntegerPreference): @user_preferences_registry.register class DownloadOrder(ChoicePreference): - section = downloader name = 'download_order' choices = VIDEO_ORDER_CHOICES default = 'playlist' @@ -125,7 +115,6 @@ class DownloadOrder(ChoicePreference): @user_preferences_registry.register class DownloadPath(StringPreference): - section = downloader name = 'download_path' default = os.path.join(settings.DATA_DIR, 'downloads') required = False @@ -133,7 +122,6 @@ class DownloadPath(StringPreference): @user_preferences_registry.register class DownloadFilePattern(StringPreference): - section = downloader name = 'download_file_pattern' default = '${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]' required = True @@ -141,7 +129,6 @@ class DownloadFilePattern(StringPreference): @user_preferences_registry.register class DownloadFormat(StringPreference): - section = downloader name = 'download_format' default = 'bestvideo+bestaudio' required = True @@ -149,39 +136,34 @@ class DownloadFormat(StringPreference): @user_preferences_registry.register class DownloadSubtitles(BooleanPreference): - section = downloader - name = 'subtitles_enabled' + name = 'download_subtitles' default = True required = True @user_preferences_registry.register class DownloadAutogeneratedSubtitles(BooleanPreference): - section = downloader - name = 'autogenerated_subtitles' + name = 'download_autogenerated_subtitles' default = False required = True @user_preferences_registry.register class DownloadAllSubtitles(BooleanPreference): - section = downloader - name = 'all_subtitles' + name = 'download_subtitles_all' default = False required = False @user_preferences_registry.register class DownloadSubtitlesLangs(StringPreference): - section = downloader - name = 'subtitles_langs' + name = 'download_subtitles_langs' default = 'en,ro' required = False @user_preferences_registry.register class DownloadSubtitlesFormat(StringPreference): - section = downloader - name = 'subtitles_format' - default = False + name = 'download_subtitles_format' + default = '' required = False diff --git a/app/YtManagerApp/management/downloader.py b/app/YtManagerApp/management/downloader.py index 70c7f38..4259630 100644 --- a/app/YtManagerApp/management/downloader.py +++ b/app/YtManagerApp/management/downloader.py @@ -14,9 +14,9 @@ log = logging.getLogger('downloader') def __get_subscription_config(sub: Subscription): user = sub.user - enabled = first_non_null(sub.auto_download, user.preferences['download_enabled']) + enabled = first_non_null(sub.auto_download, user.preferences['auto_download']) global_limit = user.preferences['download_global_limit'] - limit = first_non_null(sub.download_limit, user.preferences['download_limit_per_subscription']) + limit = first_non_null(sub.download_limit, user.preferences['download_subscription_limit']) order = first_non_null(sub.download_order, user.preferences['download_order']) order = VIDEO_ORDER_MAPPING[order] diff --git a/app/YtManagerApp/management/jobs/download_video.py b/app/YtManagerApp/management/jobs/download_video.py index b35771b..96f9cd3 100644 --- a/app/YtManagerApp/management/jobs/download_video.py +++ b/app/YtManagerApp/management/jobs/download_video.py @@ -59,9 +59,9 @@ def __build_youtube_dl_params(video: Video): 'outtmpl': output_path, 'writethumbnail': True, 'writedescription': True, - 'writesubtitles': user.preferences['subtitles_enabled'], - 'writeautomaticsub': user.preferences['autogenerated_subtitles'], - 'allsubtitles': user.preferences['all_subtitles'], + 'writesubtitles': user.preferences['download_subtitles'], + 'writeautomaticsub': user.preferences['download_autogenerated_subtitles'], + 'allsubtitles': user.preferences['download_subtitles_all'], 'postprocessors': [ { 'key': 'FFmpegMetadata' @@ -69,12 +69,12 @@ def __build_youtube_dl_params(video: Video): ] } - sub_langs = user.preferences['subtitles_langs'].split(',') + sub_langs = user.preferences['download_subtitles_langs'].split(',') sub_langs = [i.strip() for i in sub_langs] if len(sub_langs) > 0: youtube_dl_params['subtitleslangs'] = sub_langs - sub_format = user.preferences['subtitles_format'] + sub_format = user.preferences['download_subtitles_format'] if len(sub_format) > 0: youtube_dl_params['subtitlesformat'] = sub_format @@ -93,7 +93,7 @@ def download_video(video: Video, attempt: int = 1): _lock.acquire() try: - max_attempts = user.preferences['download_max_attempts'] + max_attempts = user.preferences['max_download_attempts'] youtube_dl_params, output_path = __build_youtube_dl_params(video) with youtube_dl.YoutubeDL(youtube_dl_params) as yt: diff --git a/app/YtManagerApp/management/jobs/synchronize.py b/app/YtManagerApp/management/jobs/synchronize.py index bd6814c..11e49d4 100644 --- a/app/YtManagerApp/management/jobs/synchronize.py +++ b/app/YtManagerApp/management/jobs/synchronize.py @@ -73,7 +73,7 @@ def __detect_deleted(subscription: Subscription): video.downloaded_path = None # Mark watched? - if user.preferences['MarkDeletedAsWatched']: + if user.preferences['mark_deleted_as_watched']: video.watched = True video.save() diff --git a/app/YtManagerApp/models.py b/app/YtManagerApp/models.py index c500f36..979bee2 100644 --- a/app/YtManagerApp/models.py +++ b/app/YtManagerApp/models.py @@ -30,136 +30,6 @@ VIDEO_ORDER_MAPPING = { } -class UserSettings(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE) - - mark_deleted_as_watched = models.BooleanField( - null=True, blank=True, - help_text='When a downloaded video is deleted from the system, it will be marked as \'watched\'.') - - delete_watched = models.BooleanField( - null=True, blank=True, - help_text='Videos marked as watched are automatically deleted.') - - auto_download = models.BooleanField( - null=True, blank=True, - help_text='Enables or disables automatic downloading.') - - download_global_limit = models.IntegerField( - null=True, blank=True, - help_text='Limits the total number of videos downloaded (-1 = no limit).') - - download_subscription_limit = models.IntegerField( - null=True, blank=True, - help_text='Limits the number of videos downloaded per subscription (-1 = no limit). ' - ' This setting can be overriden for each individual subscription in the subscription edit dialog.') - - download_order = models.CharField( - null=True, blank=True, - max_length=100, - choices=VIDEO_ORDER_CHOICES, - help_text='The order in which videos will be downloaded.' - ) - - download_path = models.CharField( - null=True, blank=True, - max_length=1024, - help_text='Path on the disk where downloaded videos are stored. ' - ' You can use environment variables using syntax: ${env:...}' - ) - - download_file_pattern = models.CharField( - null=True, blank=True, - max_length=1024, - help_text='A pattern which describes how downloaded files are organized. Extensions are automatically appended.' - ' You can use the following fields, using the ${field} syntax:' - ' channel, channel_id, playlist, playlist_id, playlist_index, title, id.' - ' Example: ${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]') - - download_format = models.CharField( - null=True, blank=True, - max_length=256, - help_text='Download format that will be passed to youtube-dl. ' - ' See the ' - ' youtube-dl documentation for more details.') - - download_subtitles = models.BooleanField( - null=True, blank=True, - help_text='Enable downloading subtitles for the videos.' - ' The flag is passed directly to youtube-dl. You can find more information' - ' here.') - - download_autogenerated_subtitles = models.BooleanField( - null=True, blank=True, - help_text='Enables downloading the automatically generated subtitle.' - ' The flag is passed directly to youtube-dl. You can find more information' - ' here.') - - download_subtitles_all = models.BooleanField( - null=True, blank=True, - help_text='If enabled, all the subtitles in all the available languages will be downloaded.' - ' The flag is passed directly to youtube-dl. You can find more information' - ' here.') - - download_subtitles_langs = models.CharField( - null=True, blank=True, - max_length=250, - help_text='Comma separated list of languages for which subtitles will be downloaded.' - ' The flag is passed directly to youtube-dl. You can find more information' - ' here.') - - download_subtitles_format = models.CharField( - null=True, blank=True, - max_length=100, - help_text='Subtitles format preference. Examples: srt/ass/best' - ' The flag is passed directly to youtube-dl. You can find more information' - ' here.') - - @staticmethod - def find_by_user(user: User): - result = UserSettings.objects.filter(user=user) - if len(result) > 0: - return result.first() - return None - - def __str__(self): - return str(self.user) - - def to_dict(self): - ret = {} - - if self.mark_deleted_as_watched is not None: - ret['MarkDeletedAsWatched'] = self.mark_deleted_as_watched - if self.delete_watched is not None: - ret['DeleteWatched'] = self.delete_watched - if self.auto_download is not None: - ret['AutoDownload'] = self.auto_download - if self.download_global_limit is not None: - ret['DownloadGlobalLimit'] = self.download_global_limit - if self.download_subscription_limit is not None: - ret['DownloadSubscriptionLimit'] = self.download_subscription_limit - if self.download_order is not None: - ret['DownloadOrder'] = self.download_order - if self.download_path is not None: - ret['DownloadPath'] = self.download_path - if self.download_file_pattern is not None: - ret['DownloadFilePattern'] = self.download_file_pattern - if self.download_format is not None: - ret['DownloadFormat'] = self.download_format - if self.download_subtitles is not None: - ret['DownloadSubtitles'] = self.download_subtitles - if self.download_autogenerated_subtitles is not None: - ret['DownloadAutogeneratedSubtitles'] = self.download_autogenerated_subtitles - if self.download_subtitles_all is not None: - ret['DownloadSubtitlesAll'] = self.download_subtitles_all - if self.download_subtitles_langs is not None: - ret['DownloadSubtitlesLangs'] = self.download_subtitles_langs - if self.download_subtitles_format is not None: - ret['DownloadSubtitlesFormat'] = self.download_subtitles_format - - return ret - - class SubscriptionFolder(models.Model): name = models.CharField(null=False, max_length=250) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True) diff --git a/app/YtManagerApp/views/first_time.py b/app/YtManagerApp/views/first_time.py index 9b20dd8..e597f73 100644 --- a/app/YtManagerApp/views/first_time.py +++ b/app/YtManagerApp/views/first_time.py @@ -138,8 +138,8 @@ class Step3ConfigureView(WizardStepMixin, FormView): initial = super().get_initial() initial['allow_registrations'] = appconfig.allow_registrations initial['sync_schedule'] = appconfig.sync_schedule - initial['auto_download'] = self.request.user.preferences['downloader__auto_enabled'] - initial['download_location'] = self.request.user.preferences['downloader__download_path'] + initial['auto_download'] = self.request.user.preferences['auto_download'] + initial['download_location'] = self.request.user.preferences['download_path'] return initial def form_valid(self, form): @@ -153,11 +153,11 @@ class Step3ConfigureView(WizardStepMixin, FormView): auto_download = form.cleaned_data['auto_download'] if auto_download is not None: - self.request.user.preferences['downloader__auto_enabled'] = auto_download + self.request.user.preferences['auto_download'] = auto_download download_location = form.cleaned_data['download_location'] if download_location is not None and len(download_location) > 0: - self.request.user.preferences['downloader__download_path'] = download_location + self.request.user.preferences['download_path'] = download_location # Set initialized to true appconfig.initialized = True diff --git a/app/YtManagerApp/views/forms/settings.py b/app/YtManagerApp/views/forms/settings.py index b22e096..8700a25 100644 --- a/app/YtManagerApp/views/forms/settings.py +++ b/app/YtManagerApp/views/forms/settings.py @@ -2,23 +2,163 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, HTML, Submit from django import forms -from YtManagerApp.models import UserSettings +from YtManagerApp.dynamic_preferences_registry import MarkDeletedAsWatched, AutoDeleteWatched, AutoDownloadEnabled, \ + DownloadGlobalLimit, DownloadGlobalSizeLimit, DownloadSubscriptionLimit, DownloadMaxAttempts, DownloadOrder, \ + DownloadPath, DownloadFilePattern, DownloadFormat, DownloadSubtitles, DownloadAutogeneratedSubtitles, \ + DownloadAllSubtitles, DownloadSubtitlesLangs, DownloadSubtitlesFormat +from YtManagerApp.management.appconfig import appconfig +from YtManagerApp.models import VIDEO_ORDER_CHOICES -class SettingsForm(forms.ModelForm): - class Meta: - model = UserSettings - exclude = ['user'] +class SettingsForm(forms.Form): + + mark_deleted_as_watched = forms.BooleanField( + help_text='When a downloaded video is deleted from the system, it will be marked as \'watched\'.', + initial=MarkDeletedAsWatched.default, + required=False + ) + + automatically_delete_watched = forms.BooleanField( + help_text='Videos marked as watched are automatically deleted.', + initial=AutoDeleteWatched.default, + required=False + ) + + auto_download = forms.BooleanField( + help_text='Enables or disables automatic downloading.', + initial=AutoDownloadEnabled.default, + required=False + ) + + download_global_limit = forms.IntegerField( + help_text='Limits the total number of videos downloaded (-1/unset = no limit).', + initial=DownloadGlobalLimit.default, + required=False + ) + + download_global_size_limit = forms.IntegerField( + help_text='Limits the total amount of space used in MB (-1/unset = no limit).', + initial=DownloadGlobalSizeLimit.default, + required=False + ) + + download_subscription_limit = forms.IntegerField( + help_text='Limits the number of videos downloaded per subscription (-1/unset = no limit). ' + ' This setting can be overriden for each individual subscription in the subscription edit dialog.', + initial=DownloadSubscriptionLimit.default, + required=False + ) + + max_download_attempts = forms.IntegerField( + help_text='How many times to attempt downloading a video until giving up.', + initial=DownloadMaxAttempts.default, + min_value=1, + required=True + ) + + download_order = forms.ChoiceField( + help_text='The order in which videos will be downloaded.', + choices=VIDEO_ORDER_CHOICES, + initial=DownloadOrder.default, + required=True + ) + + download_path = forms.CharField( + help_text='Path on the disk where downloaded videos are stored. ' + 'You can use environment variables using syntax: ${env:...}', + initial=DownloadPath.default, + max_length=1024, + required=True + ) + + download_file_pattern = forms.CharField( + help_text='A pattern which describes how downloaded files are organized. Extensions are automatically appended.' + ' You can use the following fields, using the ${field} syntax:' + ' channel, channel_id, playlist, playlist_id, playlist_index, title, id.' + ' Example: ${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]', + initial=DownloadFilePattern.default, + max_length=1024, + required=True + ) + + download_format = forms.CharField( + help_text='Download format that will be passed to youtube-dl. ' + ' See the ' + ' youtube-dl documentation for more details.', + initial=DownloadFormat.default, + required=True + ) + + download_subtitles = forms.BooleanField( + help_text='Enable downloading subtitles for the videos.' + ' The flag is passed directly to youtube-dl. You can find more information' + ' here.', + initial=DownloadSubtitles.default, + required=False + ) + + download_autogenerated_subtitles = forms.BooleanField( + help_text='Enables downloading the automatically generated subtitle.' + ' The flag is passed directly to youtube-dl. You can find more information' + ' here.', + initial=DownloadAutogeneratedSubtitles.default, + required=False + ) + + download_subtitles_all = forms.BooleanField( + help_text='If enabled, all the subtitles in all the available languages will be downloaded.' + ' The flag is passed directly to youtube-dl. You can find more information' + ' here.', + initial=DownloadAllSubtitles.default, + required=False + ) + + download_subtitles_langs = forms.CharField( + help_text='Comma separated list of languages for which subtitles will be downloaded.' + ' The flag is passed directly to youtube-dl. You can find more information' + ' here.', + initial=DownloadSubtitlesLangs.default, + required=False + ) + + download_subtitles_format = forms.CharField( + help_text='Subtitles format preference. Examples: srt/ass/best' + ' The flag is passed directly to youtube-dl. You can find more information' + ' here.', + initial=DownloadSubtitlesFormat.default, + required=False + ) + + ALL_PROPS = [ + 'mark_deleted_as_watched', + 'automatically_delete_watched', + + 'auto_download', + 'download_path', + 'download_file_pattern', + 'download_format', + 'download_order', + 'download_global_limit', + 'download_global_size_limit', + 'download_subscription_limit', + 'max_download_attempts', + + 'download_subtitles', + 'download_subtitles_langs', + 'download_subtitles_all', + 'download_autogenerated_subtitles', + 'download_subtitles_format', + ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_class = 'form-horizontal' - self.helper.label_class = 'col-lg-3' - self.helper.field_class = 'col-lg-9' + self.helper.label_class = 'col-lg-6' + self.helper.field_class = 'col-lg-6' self.helper.layout = Layout( 'mark_deleted_as_watched', - 'delete_watched', + 'automatically_delete_watched', HTML('

Download settings

'), 'auto_download', 'download_path', @@ -26,7 +166,9 @@ class SettingsForm(forms.ModelForm): 'download_format', 'download_order', 'download_global_limit', + 'download_global_size_limit', 'download_subscription_limit', + 'max_download_attempts', HTML('

Subtitles download settings

'), 'download_subtitles', 'download_subtitles_langs', @@ -36,6 +178,16 @@ class SettingsForm(forms.ModelForm): Submit('submit', value='Save') ) + @staticmethod + def get_initials(user): + return { + x: user.preferences[x] for x in SettingsForm.ALL_PROPS + } + + def save(self, user): + for prop in SettingsForm.ALL_PROPS: + user.preferences[prop] = self.cleaned_data[prop] + class AdminSettingsForm(forms.Form): @@ -78,3 +230,29 @@ class AdminSettingsForm(forms.Form): 'scheduler_concurrency', Submit('submit', value='Save') ) + + @staticmethod + def get_initials(): + return { + 'api_key': appconfig.youtube_api_key, + 'allow_registrations': appconfig.allow_registrations, + 'sync_schedule': appconfig.sync_schedule, + 'scheduler_concurrency': appconfig.concurrency, + } + + def save(self): + api_key = self.cleaned_data['api_key'] + if api_key is not None and len(api_key) > 0: + appconfig.youtube_api_key = api_key + + allow_registrations = self.cleaned_data['allow_registrations'] + if allow_registrations is not None: + appconfig.allow_registrations = allow_registrations + + sync_schedule = self.cleaned_data['sync_schedule'] + if sync_schedule is not None and len(sync_schedule) > 0: + appconfig.sync_schedule = sync_schedule + + concurrency = self.cleaned_data['scheduler_concurrency'] + if concurrency is not None: + appconfig.concurrency = concurrency diff --git a/app/YtManagerApp/views/settings.py b/app/YtManagerApp/views/settings.py index 15819f8..a284d41 100644 --- a/app/YtManagerApp/views/settings.py +++ b/app/YtManagerApp/views/settings.py @@ -1,22 +1,24 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseForbidden from django.urls import reverse_lazy -from django.views.generic import UpdateView, FormView +from django.views.generic import FormView -from YtManagerApp.management.appconfig import appconfig -from YtManagerApp.models import UserSettings from YtManagerApp.views.forms.settings import SettingsForm, AdminSettingsForm -class SettingsView(LoginRequiredMixin, UpdateView): +class SettingsView(LoginRequiredMixin, FormView): form_class = SettingsForm - model = UserSettings template_name = 'YtManagerApp/settings.html' success_url = reverse_lazy('home') - def get_object(self, queryset=None): - obj, _ = self.model.objects.get_or_create(user=self.request.user) - return obj + def get_initial(self): + initial = super().get_initial() + initial.update(SettingsForm.get_initials(self.request.user)) + return initial + + def form_valid(self, form): + form.save(self.request.user) + return super().form_valid(form) class AdminSettingsView(LoginRequiredMixin, FormView): @@ -37,27 +39,9 @@ class AdminSettingsView(LoginRequiredMixin, FormView): def get_initial(self): initial = super().get_initial() - initial['api_key'] = appconfig.youtube_api_key - initial['allow_registrations'] = appconfig.allow_registrations - initial['sync_schedule'] = appconfig.sync_schedule - initial['scheduler_concurrency'] = appconfig.concurrency + initial.update(AdminSettingsForm.get_initials()) return initial def form_valid(self, form): - api_key = form.cleaned_data['api_key'] - if api_key is not None and len(api_key) > 0: - appconfig.youtube_api_key = api_key - - allow_registrations = form.cleaned_data['allow_registrations'] - if allow_registrations is not None: - appconfig.allow_registrations = allow_registrations - - sync_schedule = form.cleaned_data['sync_schedule'] - if sync_schedule is not None and len(sync_schedule) > 0: - appconfig.sync_schedule = sync_schedule - - concurrency = form.cleaned_data['scheduler_concurrency'] - if concurrency is not None: - appconfig.concurrency = concurrency - + form.save() return super().form_valid(form)