Implemented first time setup wizard. There are still some problems to be solved.
@ -7,7 +7,7 @@ from django.conf import settings as dj_settings
|
||||
from .management.appconfig import global_prefs
|
||||
from .management.jobs.synchronize import schedule_synchronize_global
|
||||
from .scheduler import initialize_scheduler
|
||||
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
def __initialize_logger():
|
||||
log_dir = os.path.join(dj_settings.DATA_DIR, 'logs')
|
||||
@ -29,8 +29,12 @@ def __initialize_logger():
|
||||
def main():
|
||||
__initialize_logger()
|
||||
|
||||
if global_prefs['hidden__initialized']:
|
||||
initialize_scheduler()
|
||||
schedule_synchronize_global()
|
||||
try:
|
||||
if global_prefs['hidden__initialized']:
|
||||
initialize_scheduler()
|
||||
schedule_synchronize_global()
|
||||
except OperationalError:
|
||||
# Settings table is not created when running migrate or makemigrations, so just don't do anything in this case.
|
||||
pass
|
||||
|
||||
logging.info('Initialization complete.')
|
||||
|
@ -4,6 +4,8 @@ from dynamic_preferences.registries import global_preferences_registry
|
||||
from dynamic_preferences.users.registries import user_preferences_registry
|
||||
|
||||
from YtManagerApp.models import VIDEO_ORDER_CHOICES
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
# we create some section objects to link related preferences together
|
||||
|
||||
@ -83,7 +85,7 @@ class AutoDeleteWatched(BooleanPreference):
|
||||
@user_preferences_registry.register
|
||||
class AutoDownloadEnabled(BooleanPreference):
|
||||
section = downloader
|
||||
name = 'download_enabled'
|
||||
name = 'auto_enabled'
|
||||
default = True
|
||||
required = True
|
||||
|
||||
@ -91,7 +93,7 @@ class AutoDownloadEnabled(BooleanPreference):
|
||||
@user_preferences_registry.register
|
||||
class DownloadGlobalLimit(IntegerPreference):
|
||||
section = downloader
|
||||
name = 'download_global_limit'
|
||||
name = 'global_limit'
|
||||
default = None
|
||||
required = False
|
||||
|
||||
@ -107,7 +109,7 @@ class DownloadGlobalSizeLimit(IntegerPreference):
|
||||
@user_preferences_registry.register
|
||||
class DownloadSubscriptionLimit(IntegerPreference):
|
||||
section = downloader
|
||||
name = 'download_limit_per_subscription'
|
||||
name = 'limit_per_subscription'
|
||||
default = 5
|
||||
required = False
|
||||
|
||||
@ -115,7 +117,7 @@ class DownloadSubscriptionLimit(IntegerPreference):
|
||||
@user_preferences_registry.register
|
||||
class DownloadMaxAttempts(IntegerPreference):
|
||||
section = downloader
|
||||
name = 'download_max_attempts'
|
||||
name = 'max_download_attempts'
|
||||
default = 3
|
||||
required = True
|
||||
|
||||
@ -133,7 +135,7 @@ class DownloadOrder(ChoicePreference):
|
||||
class DownloadPath(StringPreference):
|
||||
section = downloader
|
||||
name = 'download_path'
|
||||
default = None
|
||||
default = os.path.join(settings.DATA_DIR, 'downloads')
|
||||
required = False
|
||||
|
||||
|
||||
|
@ -149,7 +149,7 @@ def synchronize_subscription(subscription: Subscription):
|
||||
|
||||
|
||||
def schedule_synchronize_global():
|
||||
trigger = CronTrigger.from_crontab(global_prefs['synchronization_schedule'])
|
||||
trigger = CronTrigger.from_crontab(global_prefs['scheduler__synchronization_schedule'])
|
||||
job = scheduler.scheduler.add_job(synchronize, trigger, max_instances=1, coalesce=True)
|
||||
log.info('Scheduled synchronize job job=%s', job.id)
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from django.conf import settings
|
||||
|
||||
from YtManagerApp.management.appconfig import global_prefs
|
||||
|
||||
scheduler: BackgroundScheduler = None
|
||||
|
||||
@ -13,7 +14,7 @@ def initialize_scheduler():
|
||||
executors = {
|
||||
'default': {
|
||||
'type': 'threadpool',
|
||||
'max_workers': settings.SCHEDULER_CONCURRENCY
|
||||
'max_workers': global_prefs['scheduler__concurrency']
|
||||
}
|
||||
}
|
||||
job_defaults = {
|
||||
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 12 KiB |
@ -5,7 +5,7 @@
|
||||
|
||||
<div class="container">
|
||||
<h1>Done!</h1>
|
||||
<p>The application is now ready to use!</p>
|
||||
<p>The setup is finished, and the application is now ready to use!</p>
|
||||
{% crispy form %}
|
||||
</div>
|
||||
|
@ -0,0 +1,66 @@
|
||||
{% extends "YtManagerApp/master_default.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>Step 1: Set up YouTube API key (optional)</h1>
|
||||
<p>This program uses the YouTube API in order to obtain information about videos, channels and playlists.
|
||||
In order to access this API, YouTube requires that we register on their site and request an API key. YouTube
|
||||
uses this key in order to limit the number of requests made to their platform, in order to prevent abuses.</p>
|
||||
|
||||
<p>To use this program, it is <em>recommended</em> that you create an API key for your own account. While a key
|
||||
is already provided the developer, there is a chance that the quota limits will be reached, which will prevent
|
||||
this program from reaching YouTube. Follow the steps below, or press <strong>Skip</strong> to use the provided key.</p>
|
||||
|
||||
<h4>
|
||||
<a data-toggle="collapse" href="#HowToObtainKey" role="button" aria-expanded="false" aria-controls="HowToObtainKey">
|
||||
<span class="typcn typcn-arrow-sorted-down"></span>How to obtain a key
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
<div class="collapse" id="HowToObtainKey">
|
||||
<ol>
|
||||
<li>Visit <a href="https://developers.google.com/">https://developers.google.com/</a>, log in or create an account, if necessary.</li>
|
||||
<li>Go to <a href="https://console.developers.google.com/project"></a>, and click on the <strong>Create project</strong> button.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_create_project.png' %}">
|
||||
</li>
|
||||
<li>Give a name to the project, and then click on <strong>Create</strong>. Wait for a few seconds, until Google finishes creating the project.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_project_name.png' %}">
|
||||
</li>
|
||||
<li>Make sure the newly created project is selected in the top bar and then, in the left sidebar,
|
||||
go to <strong>APIs & Services</strong> - <strong>Library</strong>.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_goto_apis.png' %}">
|
||||
</li>
|
||||
<li>Find and click on the <strong>YouTube Data API v3</strong> from the list.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_select_ytapi.png' %}">
|
||||
</li>
|
||||
<li>Click <strong>Enable</strong> to enable the YouTube API for your account.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_enable_ytapi.png' %}">
|
||||
</li>
|
||||
<li>In the navigation sidebar, go to the <strong>Credentials</strong> page.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_goto_credentials.png' %}">
|
||||
</li>
|
||||
<li>Click on <strong>Create credentials</strong>.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_create_credential.png' %}">
|
||||
</li>
|
||||
<li>Fill the requested information; we will need access to the <strong>YouTube Data v3</strong>,
|
||||
we will be calling the API from a <strong>Web server</strong>, and we will access the <strong>Public data</strong>.
|
||||
After filling the information, click on the <strong>What credentials do I need?</strong> button.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_create_credential_options.png' %}">
|
||||
</li>
|
||||
<li>Copy the created key in the box below, and then hit <strong>Done</strong>.
|
||||
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_done.png' %}">
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{% crispy form %}
|
||||
|
||||
<a href="{% url 'first_time_2' %}">Skip</a>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock body %}
|
@ -0,0 +1,15 @@
|
||||
{% extends "YtManagerApp/master_default.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>Step 2: Create an administrator account</h1>
|
||||
|
||||
{% crispy form %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock body %}
|
@ -0,0 +1,17 @@
|
||||
{% extends "YtManagerApp/master_default.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>Step 3: Configure the server</h1>
|
||||
|
||||
<p>Here you can customize some basic options for the application. There are many more options which can be changed in the settings page.</p>
|
||||
|
||||
{% crispy form %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock body %}
|
@ -24,6 +24,7 @@ from .views.auth import ExtendedLoginView, RegisterView, RegisterDoneView
|
||||
from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal, \
|
||||
CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal, ImportSubscriptionsModal
|
||||
from .views.settings import SettingsView
|
||||
from .views import first_time
|
||||
|
||||
urlpatterns = [
|
||||
# Authentication URLs
|
||||
@ -59,4 +60,11 @@ urlpatterns = [
|
||||
path('', index, name='home'),
|
||||
path('settings/', SettingsView.as_view(), name='settings'),
|
||||
|
||||
# First time setup
|
||||
path('first_time/step0_welcome', first_time.Step0WelcomeView.as_view(), name='first_time_0'),
|
||||
path('first_time/step1_apikey', first_time.Step1ApiKeyView.as_view(), name='first_time_1'),
|
||||
path('first_time/step2_admin', first_time.Step2CreateAdminUserView.as_view(), name='first_time_2'),
|
||||
path('first_time/step3_config', first_time.Step3ConfigureView.as_view(), name='first_time_3'),
|
||||
path('first_time/done', first_time.DoneView.as_view(), name='first_time_done'),
|
||||
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
@ -3,49 +3,165 @@ from crispy_forms.layout import Layout, HTML, Submit
|
||||
from django import forms
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import UpdateView
|
||||
|
||||
from django.views.generic import UpdateView, FormView
|
||||
from django.shortcuts import render, redirect
|
||||
from YtManagerApp.views.auth import RegisterView
|
||||
from YtManagerApp.models import UserSettings
|
||||
|
||||
from YtManagerApp.management.appconfig import global_prefs
|
||||
from django.http import HttpResponseForbidden
|
||||
|
||||
class SettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserSettings
|
||||
exclude = ['user']
|
||||
|
||||
class ProtectInitializedMixin(object):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if global_prefs['hidden__initialized']:
|
||||
return redirect('home')
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if global_prefs['hidden__initialized']:
|
||||
return HttpResponseForbidden()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
# Step 0: welcome screen
|
||||
#
|
||||
class Step0WelcomeForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.layout = Layout(
|
||||
Submit('submit', value='Continue')
|
||||
)
|
||||
|
||||
|
||||
class Step0WelcomeView(ProtectInitializedMixin, FormView):
|
||||
template_name = 'YtManagerApp/first_time_setup/step0_welcome.html'
|
||||
form_class = Step0WelcomeForm
|
||||
success_url = reverse_lazy('first_time_1')
|
||||
|
||||
|
||||
#
|
||||
# Step 1: setup API key
|
||||
#
|
||||
class Step1ApiKeyForm(forms.Form):
|
||||
api_key = forms.CharField(label="YouTube API Key:")
|
||||
|
||||
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')
|
||||
'api_key',
|
||||
Submit('submit', value='Continue'),
|
||||
)
|
||||
|
||||
|
||||
class SettingsView(LoginRequiredMixin, UpdateView):
|
||||
form_class = SettingsForm
|
||||
model = UserSettings
|
||||
template_name = 'YtManagerApp/settings.html'
|
||||
success_url = reverse_lazy('home')
|
||||
class Step1ApiKeyView(ProtectInitializedMixin, FormView):
|
||||
template_name = 'YtManagerApp/first_time_setup/step1_apikey.html'
|
||||
form_class = Step1ApiKeyForm
|
||||
success_url = reverse_lazy('first_time_2')
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
obj, _ = self.model.objects.get_or_create(user=self.request.user)
|
||||
return obj
|
||||
def form_valid(self, form):
|
||||
key = form.cleaned_data['api_key']
|
||||
# TODO: validate key
|
||||
if key is not None and len(key) > 0:
|
||||
global_prefs['general__youtube_api_key'] = key
|
||||
|
||||
|
||||
#
|
||||
# Step 2: create admin user
|
||||
#
|
||||
class Step2CreateAdminUserView(ProtectInitializedMixin, RegisterView):
|
||||
template_name = 'YtManagerApp/first_time_setup/step2_admin.html'
|
||||
success_url = reverse_lazy('first_time_3')
|
||||
|
||||
|
||||
#
|
||||
# Step 3: configure server
|
||||
#
|
||||
class Step3ConfigureForm(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 Step3ConfigureView(ProtectInitializedMixin, FormView):
|
||||
template_name = 'YtManagerApp/first_time_setup/step3_configure.html'
|
||||
form_class = Step3ConfigureForm
|
||||
success_url = reverse_lazy('first_time_done')
|
||||
|
||||
def form_valid(self, form):
|
||||
allow_registrations = form.cleaned_data['allow_registrations']
|
||||
if allow_registrations is not None:
|
||||
global_prefs['general__allow_registrations'] = allow_registrations
|
||||
|
||||
sync_schedule = form.cleaned_data['sync_schedule']
|
||||
if sync_schedule is not None and len(sync_schedule) > 0:
|
||||
global_prefs['scheduler__synchronization_schedule'] = sync_schedule
|
||||
|
||||
auto_download = form.cleaned_data['auto_download']
|
||||
if auto_download is not None:
|
||||
self.request.user.preferences['downloader__auto_enabled'] = 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
|
||||
|
||||
# Set initialized to true
|
||||
global_prefs['hidden__initialized'] = True
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
#
|
||||
# Done screen
|
||||
#
|
||||
class DoneForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.layout = Layout(
|
||||
Submit('submit', value='Finish')
|
||||
)
|
||||
|
||||
|
||||
class DoneView(FormView):
|
||||
template_name = 'YtManagerApp/first_time_setup/done.html'
|
||||
form_class = DoneForm
|
||||
success_url = reverse_lazy('home')
|
||||
|
@ -5,11 +5,12 @@ 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.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 YtManagerApp.management.videos import get_videos
|
||||
from YtManagerApp.management.appconfig import global_prefs
|
||||
from YtManagerApp.models import Subscription, SubscriptionFolder, VIDEO_ORDER_CHOICES, VIDEO_ORDER_MAPPING
|
||||
from YtManagerApp.utils import youtube, subscription_file_parser
|
||||
from YtManagerApp.views.controls.modal import ModalMixin
|
||||
@ -94,6 +95,10 @@ def __tree_sub_id(sub_id):
|
||||
|
||||
|
||||
def index(request: HttpRequest):
|
||||
|
||||
if not global_prefs['hidden__initialized']:
|
||||
return redirect('first_time_0')
|
||||
|
||||
context = {
|
||||
'config_errors': settings.CONFIG_ERRORS,
|
||||
'config_warnings': settings.CONFIG_WARNINGS,
|
||||
|