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.

+ +

+ +

+ +
+
    +
  1. Visit https://developers.google.com/, log in or create an account, if necessary.
  2. +
  3. Go to , and click on the Create project button. + +
  4. +
  5. Give a name to the project, and then click on Create. Wait for a few seconds, until Google finishes creating the project. + +
  6. +
  7. Make sure the newly created project is selected in the top bar and then, in the left sidebar, + go to APIs & Services - Library. + +
  8. +
  9. Find and click on the YouTube Data API v3 from the list. + +
  10. +
  11. Click Enable to enable the YouTube API for your account. + +
  12. +
  13. In the navigation sidebar, go to the Credentials page. + +
  14. +
  15. Click on Create credentials. + +
  16. +
  17. 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. + +
  18. +
  19. Copy the created key in the box below, and then hit Done. + +
  20. +
+
+ + {% 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,