diff --git a/app/YtManagerApp/appmain.py b/app/YtManagerApp/appmain.py
index 5b7c449..f8397b8 100644
--- a/app/YtManagerApp/appmain.py
+++ b/app/YtManagerApp/appmain.py
@@ -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.')
diff --git a/app/YtManagerApp/dynamic_preferences_registry.py b/app/YtManagerApp/dynamic_preferences_registry.py
index 13b0cdd..895f158 100644
--- a/app/YtManagerApp/dynamic_preferences_registry.py
+++ b/app/YtManagerApp/dynamic_preferences_registry.py
@@ -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
diff --git a/app/YtManagerApp/management/jobs/synchronize.py b/app/YtManagerApp/management/jobs/synchronize.py
index 04741b9..d55636a 100644
--- a/app/YtManagerApp/management/jobs/synchronize.py
+++ b/app/YtManagerApp/management/jobs/synchronize.py
@@ -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)
diff --git a/app/YtManagerApp/scheduler.py b/app/YtManagerApp/scheduler.py
index 4863835..ca6958e 100644
--- a/app/YtManagerApp/scheduler.py
+++ b/app/YtManagerApp/scheduler.py
@@ -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 = {
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential.png
new file mode 100644
index 0000000..2977be5
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential_options.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential_options.png
new file mode 100644
index 0000000..57e5960
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential_options.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_project.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_project.png
new file mode 100644
index 0000000..45e1471
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_project.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_done.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_done.png
new file mode 100644
index 0000000..7de95be
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_done.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_enable_ytapi.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_enable_ytapi.png
new file mode 100644
index 0000000..236688c
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_enable_ytapi.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_apis.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_apis.png
new file mode 100644
index 0000000..bb92bdb
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_apis.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_credentials.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_credentials.png
new file mode 100644
index 0000000..9e78e53
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_credentials.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_project_name.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_project_name.png
new file mode 100644
index 0000000..ca38d6d
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_project_name.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_project.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_project.png
new file mode 100644
index 0000000..fcae29f
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_project.png differ
diff --git a/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_ytapi.png b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_ytapi.png
new file mode 100644
index 0000000..f346068
Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_ytapi.png differ
diff --git a/app/YtManagerApp/templates/YtManagerApp/first_time_setup/9_done.html b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/done.html
similarity index 72%
rename from app/YtManagerApp/templates/YtManagerApp/first_time_setup/9_done.html
rename to app/YtManagerApp/templates/YtManagerApp/first_time_setup/done.html
index 43e1fdb..a46e317 100644
--- a/app/YtManagerApp/templates/YtManagerApp/first_time_setup/9_done.html
+++ b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/done.html
@@ -5,7 +5,7 @@
Done!
-
The application is now ready to use!
+
The setup is finished, and the application is now ready to use!
{% crispy form %}
diff --git a/app/YtManagerApp/templates/YtManagerApp/first_time_setup/0_welcome.html b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step0_welcome.html
similarity index 100%
rename from app/YtManagerApp/templates/YtManagerApp/first_time_setup/0_welcome.html
rename to app/YtManagerApp/templates/YtManagerApp/first_time_setup/step0_welcome.html
diff --git a/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step1_apikey.html b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step1_apikey.html
new file mode 100644
index 0000000..9ee5997
--- /dev/null
+++ b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step1_apikey.html
@@ -0,0 +1,66 @@
+{% extends "YtManagerApp/master_default.html" %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block body %}
+
+
+
+
Step 1: Set up YouTube API key (optional)
+
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.
+
+
To use this program, it is recommended 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 Skip to use the provided key.
+
+
+
+
+
+ - Visit https://developers.google.com/, log in or create an account, if necessary.
+ - Go to , and click on the Create project button.
+
+
+ - Give a name to the project, and then click on Create. Wait for a few seconds, until Google finishes creating the project.
+
+
+ - Make sure the newly created project is selected in the top bar and then, in the left sidebar,
+ go to APIs & Services - Library.
+
+
+ - Find and click on the YouTube Data API v3 from the list.
+
+
+ - Click Enable to enable the YouTube API for your account.
+
+
+ - In the navigation sidebar, go to the Credentials page.
+
+
+ - Click on Create credentials.
+
+
+ - Fill the requested information; we will need access to the YouTube Data v3,
+ we will be calling the API from a Web server, and we will access the Public data.
+ After filling the information, click on the What credentials do I need? button.
+
+
+ - Copy the created key in the box below, and then hit Done.
+
+
+
+
+
+ {% crispy form %}
+
+
Skip
+
+
+
+{% endblock body %}
\ No newline at end of file
diff --git a/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step2_admin.html b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step2_admin.html
new file mode 100644
index 0000000..b2cdfe4
--- /dev/null
+++ b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step2_admin.html
@@ -0,0 +1,15 @@
+{% extends "YtManagerApp/master_default.html" %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block body %}
+
+
+
+
Step 2: Create an administrator account
+
+ {% crispy form %}
+
+
+
+{% endblock body %}
\ No newline at end of file
diff --git a/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step3_configure.html b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step3_configure.html
new file mode 100644
index 0000000..cec38dd
--- /dev/null
+++ b/app/YtManagerApp/templates/YtManagerApp/first_time_setup/step3_configure.html
@@ -0,0 +1,17 @@
+{% extends "YtManagerApp/master_default.html" %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block body %}
+
+
+
+
Step 3: Configure the server
+
+
Here you can customize some basic options for the application. There are many more options which can be changed in the settings page.
+
+ {% crispy form %}
+
+
+
+{% endblock body %}
\ No newline at end of file
diff --git a/app/YtManagerApp/urls.py b/app/YtManagerApp/urls.py
index a1c0aa3..ec60500 100644
--- a/app/YtManagerApp/urls.py
+++ b/app/YtManagerApp/urls.py
@@ -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)
diff --git a/app/YtManagerApp/views/first_time.py b/app/YtManagerApp/views/first_time.py
index f7a019b..aa32d2c 100644
--- a/app/YtManagerApp/views/first_time.py
+++ b/app/YtManagerApp/views/first_time.py
@@ -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('Download settings
'),
- 'auto_download',
- 'download_path',
- 'download_file_pattern',
- 'download_format',
- 'download_order',
- 'download_global_limit',
- 'download_subscription_limit',
- HTML('Subtitles download settings
'),
- '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('Server settings
'),
+ 'sync_schedule',
+ 'allow_registrations',
+ HTML('User settings
'),
+ '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')
diff --git a/app/YtManagerApp/views/index.py b/app/YtManagerApp/views/index.py
index f7cfd7a..2a0c791 100644
--- a/app/YtManagerApp/views/index.py
+++ b/app/YtManagerApp/views/index.py
@@ -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,