Merged changes from master

This commit is contained in:
2019-01-01 23:35:38 +02:00
161 changed files with 66822 additions and 663 deletions

View File

@ -1,70 +1,27 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django import forms
from django.contrib.auth import login, authenticate
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView
from django.http import HttpResponseForbidden
from django.urls import reverse_lazy
from django.views.generic import FormView, TemplateView
class ExtendedAuthenticationForm(AuthenticationForm):
remember_me = forms.BooleanField(label='Remember me', required=False, initial=False)
def clean(self):
remember_me = self.cleaned_data.get('remember_me')
if remember_me:
expiry = 3600 * 24 * 30
else:
expiry = 0
self.request.session.set_expiry(expiry)
return super().clean()
from YtManagerApp.management.appconfig import appconfig
from YtManagerApp.views.forms.auth import ExtendedAuthenticationForm, ExtendedUserCreationForm
class ExtendedLoginView(LoginView):
form_class = ExtendedAuthenticationForm
class ExtendedUserCreationForm(UserCreationForm):
email = forms.EmailField(required=False,
label='E-mail address',
help_text='The e-mail address is optional, but it is the only way to recover a lost '
'password.')
first_name = forms.CharField(max_length=30, required=False,
label='First name')
last_name = forms.CharField(max_length=150, required=False,
label='Last name')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.label_class = 'col-3'
self.helper.field_class = 'col-9'
self.helper.form_class = 'form-horizontal'
self.helper.form_method = 'post'
self.helper.form_action = reverse_lazy('register')
self.helper.add_input(Submit('submit', 'register'))
class Meta(UserCreationForm.Meta):
fields = ['username', 'email', 'first_name', 'last_name']
class RegisterView(FormView):
template_name = 'registration/register.html'
form_class = ExtendedUserCreationForm
success_url = reverse_lazy('register_done')
def form_valid(self, form):
is_first_user = (User.objects.count() == 0)
user = form.save()
if is_first_user:
user.is_staff = True
user.is_superuser = True
user.save()
form.save()
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password1')
@ -78,6 +35,12 @@ class RegisterView(FormView):
context['is_first_user'] = (User.objects.count() == 0)
return context
def post(self, request, *args, **kwargs):
if not appconfig.allow_registrations:
return HttpResponseForbidden("Registrations are disabled!")
return super().post(request, *args, **kwargs)
class RegisterDoneView(LoginRequiredMixin, TemplateView):
template_name = 'registration/register_done.html'

View File

@ -39,8 +39,8 @@ class ModalMixin(ContextMixin):
result = {'success': success}
if not success:
result['errors'] = form.errors.get_json_data(escape_html=True)
if error_msg is not None:
result['errors']['__all__'] = [{'message': error_msg}]
if error_msg is not None:
result['errors']['__all__'] = [{'message': error_msg}]
return JsonResponse(result)

View File

@ -0,0 +1,193 @@
import logging
from django.conf import settings
from django.contrib.auth import authenticate, login
from django.contrib.auth.models import User
from django.http import HttpResponseForbidden
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views.generic import FormView
from YtManagerApp.management.appconfig import appconfig
from YtManagerApp.management.jobs.synchronize import schedule_synchronize_global
from YtManagerApp.scheduler import initialize_scheduler
from YtManagerApp.views.forms.auth import ExtendedAuthenticationForm
from YtManagerApp.views.forms.first_time import WelcomeForm, ApiKeyForm, PickAdminUserForm, ServerConfigForm, DoneForm, UserCreationForm
logger = logging.getLogger("FirstTimeWizard")
class WizardStepMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get(self, request, *args, **kwargs):
# Prevent access if application is already initialized
if appconfig.initialized:
logger.debug(f"Attempted to access {request.path}, but first time setup already run. Redirected to home "
f"page.")
return redirect('home')
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if appconfig.initialized:
logger.debug(f"Attempted to post {request.path}, but first time setup already run.")
return HttpResponseForbidden()
return super().post(request, *args, **kwargs)
#
# Step 0: welcome screen
#
class Step0WelcomeView(WizardStepMixin, FormView):
template_name = 'YtManagerApp/first_time_setup/step0_welcome.html'
form_class = WelcomeForm
success_url = reverse_lazy('first_time_1')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'config_errors': settings.CONFIG_ERRORS,
'config_warnings': settings.CONFIG_WARNINGS,
})
return context
#
# Step 1: setup API key
#
class Step1ApiKeyView(WizardStepMixin, FormView):
template_name = 'YtManagerApp/first_time_setup/step1_apikey.html'
form_class = ApiKeyForm
success_url = reverse_lazy('first_time_2')
def get_initial(self):
initial = super().get_initial()
initial['api_key'] = appconfig.youtube_api_key
return initial
def form_valid(self, form):
key = form.cleaned_data['api_key']
# TODO: validate key
if key is not None and len(key) > 0:
appconfig.youtube_api_key = key
#
# Step 2: create admin user
#
class Step2SetupAdminUserView(WizardStepMixin, FormView):
template_name = 'YtManagerApp/first_time_setup/step2_admin.html'
success_url = reverse_lazy('first_time_3')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__form_class = UserCreationForm
def get_form_class(self):
return self.__form_class
def get(self, request, *args, **kwargs):
have_users = User.objects.count() > 0
have_admin = User.objects.filter(is_superuser=True).count() > 0
# Skip if admin is already logged in
if request.user.is_authenticated and request.user.is_superuser:
logger.debug("Admin user already exists and is logged in!")
return redirect(self.success_url)
# Check if an admin user already exists
elif have_admin:
logger.debug("Admin user already exists and is not logged in!")
self.__form_class = ExtendedAuthenticationForm
elif have_users and 'register' not in kwargs:
logger.debug("There are users but no admin!")
self.__form_class = PickAdminUserForm
else:
logger.debug("No admin user exists, will register a new account!")
self.__form_class = UserCreationForm
return super().get(request, *args, **kwargs)
def form_valid(self, form):
if isinstance(form, ExtendedAuthenticationForm):
login(self.request, form.get_user())
elif isinstance(form, UserCreationForm):
user = form.save()
user.is_staff = True
user.is_superuser = True
user.save()
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password1')
user = authenticate(username=username, password=password)
login(self.request, user)
elif isinstance(form, PickAdminUserForm):
user = form.cleaned_data['admin_user']
user.is_staff = True
user.is_superuser = True
user.save()
return redirect('first_time_2', assigned_success='1')
return super().form_valid(form)
#
# Step 3: configure server
#
class Step3ConfigureView(WizardStepMixin, FormView):
template_name = 'YtManagerApp/first_time_setup/step3_configure.html'
form_class = ServerConfigForm
success_url = reverse_lazy('first_time_done')
def get_initial(self):
initial = super().get_initial()
initial['allow_registrations'] = appconfig.allow_registrations
initial['sync_schedule'] = appconfig.sync_schedule
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):
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
auto_download = form.cleaned_data['auto_download']
if auto_download is not None:
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['download_path'] = download_location
# Set initialized to true
appconfig.initialized = True
# Start scheduler if not started
initialize_scheduler()
schedule_synchronize_global()
return super().form_valid(form)
#
# Done screen
#
class DoneView(FormView):
template_name = 'YtManagerApp/first_time_setup/done.html'
form_class = DoneForm
success_url = reverse_lazy('home')

View File

@ -0,0 +1,45 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django import forms
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.urls import reverse_lazy
class ExtendedAuthenticationForm(AuthenticationForm):
remember_me = forms.BooleanField(label='Remember me', required=False, initial=False)
def clean(self):
remember_me = self.cleaned_data.get('remember_me')
if remember_me:
expiry = 3600 * 24 * 30
else:
expiry = 0
self.request.session.set_expiry(expiry)
return super().clean()
class ExtendedUserCreationForm(UserCreationForm):
email = forms.EmailField(required=False,
label='E-mail address',
help_text='The e-mail address is optional, but it is the only way to recover a lost '
'password.')
first_name = forms.CharField(max_length=30, required=False,
label='First name')
last_name = forms.CharField(max_length=150, required=False,
label='Last name')
form_action = reverse_lazy('register')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.label_class = 'col-3'
self.helper.field_class = 'col-9'
self.helper.form_class = 'form-horizontal'
self.helper.form_method = 'post'
self.helper.form_action = self.form_action
self.helper.add_input(Submit('submit', 'register'))
class Meta(UserCreationForm.Meta):
fields = ['username', 'email', 'first_name', 'last_name']

View File

@ -0,0 +1,107 @@
import logging
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, HTML, Submit, Column
from django import forms
from django.contrib.auth.models import User
from django.urls import reverse_lazy
from YtManagerApp.views.forms.auth import ExtendedUserCreationForm
logger = logging.getLogger("FirstTimeWizard")
class WelcomeForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Submit('submit', value='Continue')
)
class ApiKeyForm(forms.Form):
api_key = forms.CharField(label="YouTube API Key:")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
'api_key',
Column(
Submit('submit', value='Continue'),
HTML('<a href="{% url \'first_time_2\' %}" class="btn">Skip</a>')
)
)
class UserCreationForm(ExtendedUserCreationForm):
form_action = reverse_lazy('first_time_2')
class PickAdminUserForm(forms.Form):
admin_user = forms.ModelChoiceField(
User.objects.order_by('username'),
label='User to promote to admin',
required=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
'admin_user',
Column(
Submit('submit', value='Continue'),
HTML('<a href="{% url \'first_time_2\' %}&register=1" class="btn">Register a new admin user</a>')
)
)
class ServerConfigForm(forms.Form):
allow_registrations = forms.BooleanField(
label="Allow user registrations",
help_text="Disabling this option will prevent anyone from registering to the site.",
initial=True,
required=False
)
sync_schedule = forms.CharField(
label="Synchronization schedule",
help_text="How often should the application look for new videos.",
initial="5 * * * *",
required=True
)
auto_download = forms.BooleanField(
label="Download videos automatically",
required=False
)
download_location = forms.CharField(
label="Download location",
help_text="Location on the server where videos are downloaded.",
required=True
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
HTML('<h3>Server settings</h3>'),
'sync_schedule',
'allow_registrations',
HTML('<h3>User settings</h3>'),
'auto_download',
'download_location',
Submit('submit', value='Continue'),
)
class DoneForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Submit('submit', value='Finish')
)

View File

@ -0,0 +1,258 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, HTML, Submit
from django import forms
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.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: <code>${env:...}</code>',
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 <code>${field}</code> syntax:'
' channel, channel_id, playlist, playlist_id, playlist_index, title, id.'
' Example: <code>${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]</code>',
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 <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#format-selection">'
' youtube-dl documentation</a> 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'
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.',
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'
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.',
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'
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.',
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'
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.',
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'
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.',
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.layout = Layout(
'mark_deleted_as_watched',
'automatically_delete_watched',
HTML('<h2>Download settings</h2>'),
'auto_download',
'download_path',
'download_file_pattern',
'download_format',
'download_order',
'download_global_limit',
'download_global_size_limit',
'download_subscription_limit',
'max_download_attempts',
HTML('<h2>Subtitles download settings</h2>'),
'download_subtitles',
'download_subtitles_langs',
'download_subtitles_all',
'download_autogenerated_subtitles',
'download_subtitles_format',
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):
api_key = forms.CharField(label="YouTube API key")
allow_registrations = forms.BooleanField(
label="Allow user registrations",
help_text="Disabling this option will prevent anyone from registering to the site.",
initial=True,
required=False
)
sync_schedule = forms.CharField(
label="Synchronization schedule",
help_text="How often should the application look for new videos.",
initial="5 * * * *",
required=True
)
scheduler_concurrency = forms.IntegerField(
label="Synchronization concurrency",
help_text="How many jobs are executed executed in parallel. Since most jobs are I/O bound (mostly use the hard "
"drive and network), there is no significant advantage to increase it.",
initial=2,
required=True
)
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.layout = Layout(
HTML('<h2>General settings</h2>'),
'api_key',
'allow_registrations',
HTML('<h2>Scheduler settings</h2>'),
'sync_schedule',
'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

View File

@ -5,16 +5,20 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.shortcuts import render
from django.views.generic import CreateView, UpdateView, DeleteView
from django.shortcuts import render, redirect
from django.views.generic import CreateView, UpdateView, DeleteView, FormView
from django.views.generic.edit import FormMixin
from django.conf import settings
from django.core.paginator import Paginator
from YtManagerApp.management.videos import get_videos
from YtManagerApp.management.appconfig import appconfig
from YtManagerApp.models import Subscription, SubscriptionFolder, VIDEO_ORDER_CHOICES, VIDEO_ORDER_MAPPING
from YtManagerApp.utils import youtube
from YtManagerApp.utils import youtube, subscription_file_parser
from YtManagerApp.views.controls.modal import ModalMixin
from YtManagerApp.management.notification_manager import get_current_notification_id
import logging
class VideoFilterForm(forms.Form):
CHOICES_SHOW_WATCHED = (
@ -35,6 +39,13 @@ class VideoFilterForm(forms.Form):
'all': None
}
CHOICES_RESULT_COUNT = (
(25, 25),
(50, 50),
(100, 100),
(200, 200)
)
query = forms.CharField(label='', required=False)
sort = forms.ChoiceField(label='Sort:', choices=VIDEO_ORDER_CHOICES, initial='newest')
show_watched = forms.ChoiceField(label='Show only: ', choices=CHOICES_SHOW_WATCHED, initial='all')
@ -47,6 +58,11 @@ class VideoFilterForm(forms.Form):
required=False,
widget=forms.HiddenInput()
)
page = forms.IntegerField(
required=False,
widget=forms.HiddenInput()
)
results_per_page = forms.ChoiceField(label='Results per page: ', choices=CHOICES_RESULT_COUNT, initial=50)
def __init__(self, data=None):
super().__init__(data, auto_id='form_video_filter_%s')
@ -64,7 +80,9 @@ class VideoFilterForm(forms.Form):
'show_watched',
'show_downloaded',
'subscription_id',
'folder_id'
'folder_id',
'page',
'results_per_page'
)
def clean_sort(self):
@ -93,14 +111,22 @@ def __tree_sub_id(sub_id):
def index(request: HttpRequest):
if not appconfig.initialized:
return redirect('first_time_0')
context = {
'config_errors': settings.CONFIG_ERRORS,
'config_warnings': settings.CONFIG_WARNINGS,
}
if request.user.is_authenticated:
context = {
context.update({
'filter_form': VideoFilterForm(),
'current_notification_id': get_current_notification_id(),
}
})
return render(request, 'YtManagerApp/index.html', context)
else:
return render(request, 'YtManagerApp/index_unauthenticated.html')
return render(request, 'YtManagerApp/index_unauthenticated.html', context)
@login_required
@ -143,6 +169,9 @@ def ajax_get_videos(request: HttpRequest):
only_downloaded=form.cleaned_data['show_downloaded']
)
paginator = Paginator(videos, form.cleaned_data['results_per_page'])
videos = paginator.get_page(form.cleaned_data['page'])
context = {
'videos': videos
}
@ -218,6 +247,10 @@ class DeleteFolderModal(LoginRequiredMixin, ModalMixin, FormMixin, DeleteView):
model = SubscriptionFolder
form_class = DeleteFolderForm
def __init__(self, *args, **kwargs):
self.object = None
super().__init__(*args, **kwargs)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
@ -237,7 +270,7 @@ class CreateSubscriptionForm(forms.ModelForm):
class Meta:
model = Subscription
fields = ['parent_folder', 'auto_download',
'download_limit', 'download_order', 'delete_after_watched']
'download_limit', 'download_order', "automatically_delete_watched"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -304,7 +337,7 @@ class UpdateSubscriptionForm(forms.ModelForm):
class Meta:
model = Subscription
fields = ['name', 'parent_folder', 'auto_download',
'download_limit', 'download_order', 'delete_after_watched']
'download_limit', 'download_order', "automatically_delete_watched"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -337,6 +370,10 @@ class DeleteSubscriptionModal(LoginRequiredMixin, ModalMixin, FormMixin, DeleteV
model = Subscription
form_class = DeleteSubscriptionForm
def __init__(self, *args, **kwargs):
self.object = None
super().__init__(*args, **kwargs)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
@ -348,3 +385,101 @@ class DeleteSubscriptionModal(LoginRequiredMixin, ModalMixin, FormMixin, DeleteV
def form_valid(self, form):
self.object.delete_subscription(keep_downloaded_videos=form.cleaned_data['keep_downloaded_videos'])
return super().form_valid(form)
class ImportSubscriptionsForm(forms.Form):
TRUE_FALSE_CHOICES = (
(None, '(default)'),
(True, 'Yes'),
(False, 'No')
)
VIDEO_ORDER_CHOICES_WITH_EMPTY = (
('', '(default)'),
*VIDEO_ORDER_CHOICES,
)
file = forms.FileField(label='File to import',
help_text='Supported file types: OPML, subscription list')
parent_folder = forms.ModelChoiceField(SubscriptionFolder.objects, required=False)
auto_download = forms.ChoiceField(choices=TRUE_FALSE_CHOICES, required=False)
download_limit = forms.IntegerField(required=False)
download_order = forms.ChoiceField(choices=VIDEO_ORDER_CHOICES_WITH_EMPTY, required=False)
delete_after_watched = forms.ChoiceField(choices=TRUE_FALSE_CHOICES, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.yt_api = youtube.YoutubeAPI.build_public()
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
'file',
'parent_folder',
HTML('<hr>'),
HTML('<h5>Download configuration overloads</h5>'),
'auto_download',
'download_limit',
'download_order',
'delete_after_watched'
)
def __clean_empty_none(self, name: str):
data = self.cleaned_data[name]
if isinstance(data, str) and len(data) == 0:
return None
return data
def __clean_boolean(self, name: str):
data = self.cleaned_data[name]
if isinstance(data, str) and len(data) == 0:
return None
if isinstance(data, str):
return data == 'True'
return data
def clean_auto_download(self):
return self.__clean_boolean('auto_download')
def clean_delete_after_watched(self):
return self.__clean_boolean('delete_after_watched')
def clean_download_order(self):
return self.__clean_empty_none('download_order')
class ImportSubscriptionsModal(LoginRequiredMixin, ModalMixin, FormView):
template_name = 'YtManagerApp/controls/subscriptions_import_modal.html'
form_class = ImportSubscriptionsForm
def form_valid(self, form):
file = form.cleaned_data['file']
# Parse file
try:
url_list = list(subscription_file_parser.parse(file))
except subscription_file_parser.FormatNotSupportedError:
return super().modal_response(form, success=False,
error_msg="The file could not be parsed! "
"Possible problems: format not supported, file is malformed.")
print(form.cleaned_data)
# Create subscriptions
api = youtube.YoutubeAPI.build_public()
for url in url_list:
sub = Subscription()
sub.user = self.request.user
sub.parent_folder = form.cleaned_data['parent_folder']
sub.auto_download = form.cleaned_data['auto_download']
sub.download_limit = form.cleaned_data['download_limit']
sub.download_order = form.cleaned_data['download_order']
sub.automatically_delete_watched = form.cleaned_data["automatically_delete_watched"]
try:
sub.fetch_from_url(url, api)
except Exception as e:
logging.error("Import subscription error - error processing URL %s: %s", url, e)
continue
sub.save()
return super().form_valid(form)

View File

@ -1,51 +1,49 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, HTML, Submit
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseForbidden
from django.urls import reverse_lazy
from django.views.generic import UpdateView
from django.views.generic import FormView
from YtManagerApp.models import UserSettings
from YtManagerApp.management.jobs.synchronize import schedule_synchronize_global
from YtManagerApp.views.forms.settings import SettingsForm, AdminSettingsForm
class SettingsForm(forms.ModelForm):
class Meta:
model = UserSettings
exclude = ['user']
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.layout = Layout(
'mark_deleted_as_watched',
'delete_watched',
HTML('<h2>Download settings</h2>'),
'auto_download',
'download_path',
'download_file_pattern',
'download_format',
'download_order',
'download_global_limit',
'download_subscription_limit',
HTML('<h2>Subtitles download settings</h2>'),
'download_subtitles',
'download_subtitles_langs',
'download_subtitles_all',
'download_autogenerated_subtitles',
'download_subtitles_format',
Submit('submit', value='Save')
)
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):
form_class = AdminSettingsForm
template_name = 'YtManagerApp/settings_admin.html'
success_url = reverse_lazy('home')
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.is_superuser:
return HttpResponseForbidden()
return super().post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# TODO: present stats
return context
def get_initial(self):
initial = super().get_initial()
initial.update(AdminSettingsForm.get_initials())
return initial
def form_valid(self, form):
form.save()
schedule_synchronize_global()
return super().form_valid(form)