mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
Merged changes from master
This commit is contained in:
@ -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'
|
||||
|
@ -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)
|
||||
|
||||
|
193
app/YtManagerApp/views/first_time.py
Normal file
193
app/YtManagerApp/views/first_time.py
Normal 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')
|
45
app/YtManagerApp/views/forms/auth.py
Normal file
45
app/YtManagerApp/views/forms/auth.py
Normal 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']
|
107
app/YtManagerApp/views/forms/first_time.py
Normal file
107
app/YtManagerApp/views/forms/first_time.py
Normal 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\' %}®ister=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')
|
||||
)
|
258
app/YtManagerApp/views/forms/settings.py
Normal file
258
app/YtManagerApp/views/forms/settings.py
Normal 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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user