Kaynağa Gözat

Merge pull request #44 from sleepytaco/untube-react

Re-structure Untube project to run on both prod and locally
Mohammed Khan 1 yıl önce
ebeveyn
işleme
9dadeee3d4
100 değiştirilmiş dosya ile 453 ekleme ve 251 silme
  1. 70 10
      .gitignore
  2. 31 0
      Makefile
  3. 0 50
      UnTube/production.py
  4. 0 6
      UnTube/secrets.py
  5. 0 5
      apps/main/static/fontawesome-free-5.15.3-web/css/brands.min.css
  6. 0 5
      apps/main/static/fontawesome-free-5.15.3-web/css/regular.min.css
  7. 0 5
      apps/main/static/fontawesome-free-5.15.3-web/css/solid.min.css
  8. 0 3
      apps/users/admin.py
  9. 0 0
      backend/UnTube/__init__.py
  10. 4 4
      backend/UnTube/asgi.py
  11. 33 0
      backend/UnTube/settings/__init__.py
  12. 34 0
      backend/UnTube/settings/allauth.py
  13. 23 61
      backend/UnTube/settings/base.py
  14. 17 0
      backend/UnTube/settings/custom.py
  15. 12 0
      backend/UnTube/settings/docker.py
  16. 14 0
      backend/UnTube/settings/envvars.py
  17. 48 0
      backend/UnTube/settings/pythonanywhere.py
  18. 11 0
      backend/UnTube/settings/templates/settings.dev.py
  19. 6 5
      backend/UnTube/urls.py
  20. 4 4
      backend/UnTube/wsgi.py
  21. 0 0
      backend/__init__.py
  22. 0 0
      backend/charts/__init__.py
  23. 0 0
      backend/charts/admin.py
  24. 1 1
      backend/charts/apps.py
  25. 0 0
      backend/charts/migrations/__init__.py
  26. 0 0
      backend/charts/models.py
  27. 0 0
      backend/charts/tests.py
  28. 1 1
      backend/charts/urls.py
  29. 0 0
      backend/charts/views.py
  30. 0 0
      backend/general/__init__.py
  31. 0 0
      backend/general/utils/__init__.py
  32. 20 0
      backend/general/utils/collections.py
  33. 20 0
      backend/general/utils/misc.py
  34. 17 0
      backend/general/utils/settings.py
  35. 0 0
      backend/main/__init__.py
  36. 0 0
      backend/main/admin.py
  37. 1 1
      backend/main/apps.py
  38. 0 0
      backend/main/migrations/0001_initial.py
  39. 0 0
      backend/main/migrations/0002_auto_20211204_0521.py
  40. 0 0
      backend/main/migrations/__init__.py
  41. 78 82
      backend/main/models.py
  42. 0 0
      backend/main/static/assets/imgs/dashboard.gif
  43. 0 0
      backend/main/static/assets/imgs/features.gif
  44. 0 0
      backend/main/static/assets/imgs/import.gif
  45. 0 0
      backend/main/static/assets/imgs/organize.gif
  46. 0 0
      backend/main/static/assets/imgs/playlist_stats.gif
  47. 0 0
      backend/main/static/assets/imgs/watching.gif
  48. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.css
  49. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.css.map
  50. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css
  51. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css.map
  52. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css
  53. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css.map
  54. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css
  55. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css.map
  56. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.css
  57. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.css.map
  58. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css
  59. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css.map
  60. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css
  61. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css.map
  62. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css
  63. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css.map
  64. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.css
  65. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.css.map
  66. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css
  67. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css.map
  68. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css
  69. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css.map
  70. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css
  71. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css.map
  72. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.css
  73. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.css.map
  74. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.min.css
  75. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.min.css.map
  76. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.css
  77. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.css.map
  78. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css
  79. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css.map
  80. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.js
  81. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.js.map
  82. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js
  83. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js.map
  84. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.esm.js
  85. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.esm.js.map
  86. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js
  87. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js.map
  88. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.js
  89. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.js.map
  90. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.min.js
  91. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.min.js.map
  92. 0 0
      backend/main/static/choices.js/choices.min.css
  93. 0 0
      backend/main/static/choices.js/choices.min.js
  94. 0 0
      backend/main/static/clipboard.js/clipboard.min.js
  95. 0 0
      backend/main/static/css/carousel.css
  96. 0 0
      backend/main/static/fontawesome-free-5.15.3-web/LICENSE.txt
  97. 0 0
      backend/main/static/fontawesome-free-5.15.3-web/attribution.js
  98. 6 6
      backend/main/static/fontawesome-free-5.15.3-web/css/all.css
  99. 0 0
      backend/main/static/fontawesome-free-5.15.3-web/css/all.min.css
  100. 2 2
      backend/main/static/fontawesome-free-5.15.3-web/css/brands.css

+ 70 - 10
.gitignore

@@ -1,15 +1,75 @@
-/venv/
-.azure
-/UnTube/client_secrets.json
-/db.sqlite3
-/modules
-/.idea/
-__pycache__/
+# Backup files
+*.bak
+
+# Distribution / packaging
+*.egg
+*.egg-info/
+*.manifest
+*.spec
+.Python build/
+.eggs/
+.installed.cfg
+develop-eggs/
+dist/
+downloads/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+
+# Django
 *.log
 *.pot
 *.pyc
-/static/
-.staticfiles
-.env
+/staticfiles
+__pycache__
+db.sqlite3
+media
+
+# Environments
+.venv
+/.env
+/local
+ENV/
+env.bak/
+env/
+venv
+venv.bak/
+venv/
+
+# IDEs
+.DS_Store
+.idea
+.vscode
+
+# Installer logs
+pip-delete-this-directory.txt
+pip-log.txt
+
+# mypy
+.mypy_cache
+
+# pyenv
+.python-version
+
+# pytest
+.pytest_cache
 
+# Python
+*$py.class
+*.py[cod]
 
+# Unit test / coverage reports
+*.cover
+.cache
+.coverage
+.coverage.*
+.hypothesis/
+.pytest_cache/
+.tox/
+coverage.xml
+htmlcov/
+nosetests.xml

+ 31 - 0
Makefile

@@ -0,0 +1,31 @@
+.PHONY: install
+install:
+	poetry install
+
+.PHONY: migrations
+migrations:
+	poetry run python3 -m backend.manage makemigrations
+
+.PHONY: migrate
+migrate:
+	poetry run python3 -m backend.manage migrate
+
+.PHONY: run-server
+run-server:
+	poetry run python3 -m backend.manage runserver
+
+.PHONY: shell
+shell:
+	poetry run python -m backend.manage shell
+
+.PHONY: superuser
+superuser:
+	poetry run python3 -m backend.manage createsuperuser
+
+.PHONY: update
+update: install migrate ;
+
+.PHONY: local-settings
+local-settings:
+	mkdir -p local
+	cp ./backend/UnTube/settings/templates/settings.dev.py ./local/settings.dev.py

+ 0 - 50
UnTube/production.py

@@ -1,50 +0,0 @@
-from .settings import *
-import os
-
-SECRET_KEY = os.environ['SECRET_KEY']
-YOUTUBE_V3_API_KEY = os.environ['YOUTUBE_V3_API_KEY']
-
-# configure the domain name using the environment variable found on pythonanywhere
-ALLOWED_HOSTS = ['bakaabu.pythonanywhere.com', '127.0.0.1', 'untube.it'] if 'UNTUBE' in os.environ else ['bakaabu.pythonanywhere.com', 'untube.it']
-SITE_ID = 10
-
-DEBUG = False
-CSRF_COOKIE_SECURE = True
-SESSION_COOKIE_SECURE = True
-SECURE_SSL_REDIRECT = True
-
-# WhiteNoise configuration
-MIDDLEWARE = [
-    'django.middleware.security.SecurityMiddleware',
-    # Add whitenoise middleware after the security middleware
-    'whitenoise.middleware.WhiteNoiseMiddleware',
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-]
-
-STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
-STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
-
-# DBHOST is only the server name
-hostname = os.environ['DBHOST']
-
-# Configure MySQL database on pythonanywhere
-# See https://django-mysql.readthedocs.io/en/latest/checks.html for options
-DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.mysql',
-        'NAME': f'{os.environ["DBUSER"]}${os.environ["DBNAME"]}',
-        'USER': f'{os.environ["DBUSER"]}',
-        'PASSWORD': f'{os.environ["DBPASS"]}',
-        'HOST': hostname,
-        'OPTIONS': {
-            'init_command': "SET sql_mode='STRICT_TRANS_TABLES', innodb_strict_mode=1",
-            'charset': 'utf8mb4',
-            "autocommit": True,
-        }
-    }
-}

+ 0 - 6
UnTube/secrets.py

@@ -1,6 +0,0 @@
-# Make sure you change these before production or to run this project on your own machine ;)
-SECRETS = {"SECRET_KEY": 'django-insecure-ycs22y+20sq67y(6dm6ynqw=dlhg!)%vuqpd@$p6rf3!#1h$u=',
-           "YOUTUBE_V3_API_KEY": 'AIzaSyCBOucAIJ5PdLeqzTfkTQ_6twsjNaMecS8',
-           "GOOGLE_OAUTH_CLIENT_ID": "901333803283-1lscbdmukcjj3qp0t3relmla63h6l9k6.apps.googleusercontent.com",
-           "GOOGLE_OAUTH_CLIENT_SECRET": "ekdBniL-_mAnNPwCmugfIL2q",
-           "GOOGLE_OAUTH_SCOPES": ['https://www.googleapis.com/auth/youtube']}

+ 0 - 5
apps/main/static/fontawesome-free-5.15.3-web/css/brands.min.css

@@ -1,5 +0,0 @@
-/*!
- * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- */
-@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands";font-weight:400}

+ 0 - 5
apps/main/static/fontawesome-free-5.15.3-web/css/regular.min.css

@@ -1,5 +0,0 @@
-/*!
- * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- */
-@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400}

+ 0 - 5
apps/main/static/fontawesome-free-5.15.3-web/css/solid.min.css

@@ -1,5 +0,0 @@
-/*!
- * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- */
-@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}

+ 0 - 3
apps/users/admin.py

@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.

+ 0 - 0
UnTube/__init__.py → backend/UnTube/__init__.py


+ 4 - 4
UnTube/asgi.py → backend/UnTube/asgi.py

@@ -8,14 +8,14 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
 """
 
 import os
-from dotenv import load_dotenv
+# from dotenv import load_dotenv
 from django.core.asgi import get_asgi_application
 
-settings_module = "UnTube.production" if 'UNTUBE' in os.environ else 'UnTube.settings'
+settings_module = 'backend.UnTube.settings'
 os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
 
 # to use env variables on pythonanywhere
-project_folder = os.path.expanduser('/home/bakaabu')
-load_dotenv(os.path.join(project_folder, '.env'))
+# project_folder = os.path.expanduser('/home/bakaabu')
+# load_dotenv(os.path.join(project_folder, '.env'))
 
 application = get_asgi_application()

+ 33 - 0
backend/UnTube/settings/__init__.py

@@ -0,0 +1,33 @@
+import os
+from pathlib import Path
+from split_settings.tools import include, optional
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
+# BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Namespacing our own custom environment variables
+ENVVAR_SETTINGS_PREFIX = 'UNTUBE_SETTINGS_'
+
+LOCAL_SETTINGS_PATH = os.getenv(f'{ENVVAR_SETTINGS_PREFIX}LOCAL_SETTINGS_PATH')
+
+# if local settings not specified in the environment
+if not LOCAL_SETTINGS_PATH:  # default to development mode - use local dev settings
+    LOCAL_SETTINGS_PATH = 'local/settings.dev.py'
+
+if not os.path.isabs(LOCAL_SETTINGS_PATH):  # always make sure to have the absolute path
+    LOCAL_SETTINGS_PATH = str(BASE_DIR / LOCAL_SETTINGS_PATH)
+
+include(
+    'base.py',
+    # 'logging.py',
+    # 'rest_framework.py',
+    # 'channels.py',
+    # 'aws.py',
+    'custom.py',
+    'allauth.py',
+    optional(LOCAL_SETTINGS_PATH),  # `optional` means the file may or may not exist - it is fine if it does not
+    'envvars.py',
+    'docker.py',
+    'pythonanywhere.py',
+)

+ 34 - 0
backend/UnTube/settings/allauth.py

@@ -0,0 +1,34 @@
+"""
+Django AllAuth package related settings
+"""
+
+SITE_ID = 2  # increment/decrement site ID as necessary
+LOGIN_REDIRECT_URL = '/home/'
+LOGOUT_REDIRECT_URL = '/'
+
+# Additional configuration settings
+# ACCOUNT_LOGOUT_ON_GET= True
+SOCIALACCOUNT_LOGIN_ON_GET = True
+SOCIALACCOUNT_STORE_TOKENS = True
+ACCOUNT_UNIQUE_EMAIL = True
+ACCOUNT_EMAIL_REQUIRED = True
+
+SOCIALACCOUNT_PROVIDERS = {
+    'google': {
+        # 'APP': {
+        #     'client_id': GOOGLE_OAUTH_CLIENT_ID,  # type: ignore
+        #     'secret': GOOGLE_OAUTH_CLIENT_SECRET,  # type: ignore
+        #     'key': ''
+        # },
+        # 'OAUTH_PKCE_ENABLED': True,  # valid in allauth ver > 0.47.0
+        'SCOPE': [
+            'profile',
+            'email',
+            'https://www.googleapis.com/auth/youtube',
+        ],
+        'AUTH_PARAMS': {
+            # To refresh authentication in the background, set AUTH_PARAMS['access_type'] to offline.
+            'access_type': 'offline',
+        }
+    }
+}

+ 23 - 61
UnTube/settings.py → backend/UnTube/settings/base.py

@@ -9,25 +9,8 @@ https://docs.djangoproject.com/en/3.2/topics/settings/
 For the full list of settings and their values, see
 https://docs.djangoproject.com/en/3.2/ref/settings/
 """
-import os
-from pathlib import Path
-from UnTube.secrets import SECRETS
-
-# Build paths inside the project like this: BASE_DIR / 'subdir'.
-
-# PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
-BASE_DIR = Path(__file__).resolve().parent.parent
-# BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = SECRETS['SECRET_KEY']
-YOUTUBE_V3_API_KEY = SECRETS['YOUTUBE_V3_API_KEY']
-
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
+DEBUG = False
+SECRET_KEY = NotImplemented
 
 ALLOWED_HOSTS = ['127.0.0.1']
 
@@ -39,25 +22,23 @@ INSTALLED_APPS = [
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
-
     'django.contrib.humanize',  # A set of Django template filters useful for adding a “human touch” to data.
-
     'django.contrib.sites',
+
+    'crispy_forms',
+    'import_export',
     'allauth',
     'allauth.account',
     'allauth.socialaccount',
     'allauth.socialaccount.providers.google',  # specifies google as OAuth provider
 
-    'crispy_forms',
-    'apps.users',  # has stuff related to user management in it (login, signup, show homepage, import)
-    'apps.main',  # main app, shows user their homepage
-    'apps.manage_playlists',
-    'apps.charts',
-    'apps.search',
+    'backend.users.apps.UsersConfig',  # has stuff related to user management in it (login, signup, show homepage, import)
+    'backend.main.apps.MainConfig',  # main app, shows user their homepage
+    'backend.manage_playlists.apps.ManagePlaylistsConfig',
+    'backend.charts.apps.ChartsConfig',
+    'backend.search.apps.SearchConfig',
 ]
 
-CRISPY_TEMPLATE_PACK = 'bootstrap4'
-
 MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
@@ -66,15 +47,15 @@ MIDDLEWARE = [
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
-
 ]
 
-ROOT_URLCONF = 'UnTube.urls'
+ROOT_URLCONF = 'backend.UnTube.urls'  # path to the urls.py file in root UnTube app folder
 
 TEMPLATES = [
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [BASE_DIR / 'templates'],
+        'DIRS': [BASE_DIR / 'backend' / 'templates'],  # type: ignore
+        # 'DIRS': [BASE_DIR / 'backend', BASE_DIR / 'backend' / 'templates'],
         # 'DIRS': [os.path.join(BASE_DIR, "templates")],
         'APP_DIRS': True,
         'OPTIONS': {
@@ -93,44 +74,19 @@ AUTHENTICATION_BACKENDS = [
     'allauth.account.auth_backends.AuthenticationBackend'
 ]
 
-SOCIALACCOUNT_PROVIDERS = {
-    'google': {
-        'SCOPE': [
-            'profile',
-            'email',
-            'https://www.googleapis.com/auth/youtube',
-        ],
-        'AUTH_PARAMS': {
-            # To refresh authentication in the background, set AUTH_PARAMS['access_type'] to offline.
-            'access_type': 'offline',
-        }
-    }
-}
-
-SITE_ID = 10
-
 LOGIN_URL = '/'
 
-LOGIN_REDIRECT_URL = '/home/'
-LOGOUT_REDIRECT_URL = '/home/'
-
-WSGI_APPLICATION = 'UnTube.wsgi.application'
+WSGI_APPLICATION = 'backend.UnTube.wsgi.application'  # path to the wsgi.py file in root UnTube app folder
 
 # Database
 # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
 DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': BASE_DIR / 'db.sqlite3',
+        'NAME': BASE_DIR / 'db.sqlite3',  # type: ignore
     }
 }
 
-# DATABASES = {}
-# DATABASES['default'] = dj_database_url.config(conn_max_age=600)
-
-# DATABASE_URL = os.environ['DATABASE_URL']
-# conn = psycopg2.connect(DATABASE_URL, sslmode='require')
-
 # Password validation
 # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
 AUTH_PASSWORD_VALIDATORS = [
@@ -163,12 +119,18 @@ USE_TZ = True
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/3.2/howto/static-files/
 STATIC_URL = '/static/'
-STATIC_ROOT = os.path.join(BASE_DIR, 'static')
+STATIC_ROOT = BASE_DIR / 'static'  # type: ignore
 # STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+# STATICFILES_DIRS = (
+#     BASE_DIR / 'backend' / 'main',  # type: ignore
+# )
+STATICFILES_DIRS = (
+    os.path.join(BASE_DIR, 'backend', 'main', 'static'),  # type: ignore
+)
 
 # Default primary key field type
 # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 
 MEDIA_URL = '/media/'
-MEDIA_ROOT = Path(BASE_DIR / 'media')
+MEDIA_ROOT = BASE_DIR / 'media'  # type: ignore

+ 17 - 0
backend/UnTube/settings/custom.py

@@ -0,0 +1,17 @@
+"""
+Settings specific to this application only (no Django or third party settings)
+"""
+
+YOUTUBE_V3_API_KEY = NotImplemented
+GOOGLE_OAUTH_URI = NotImplemented
+GOOGLE_OAUTH_CLIENT_ID = NotImplemented
+GOOGLE_OAUTH_CLIENT_SECRET = NotImplemented
+CRISPY_TEMPLATE_PACK = 'bootstrap4'
+
+# hosting environments
+IN_PYTHONANYWHERE = False  # PA has PYTHONANYWHERE_SITE in its env
+IN_DOCKER = False
+
+ENABLE_PRINT_STATEMENTS = False  # runs the custom print_() statement that will log outputs to terminal
+STOKEN_EXPIRATION_SECONDS = 10
+USE_ON_COMMIT_HOOK = True

+ 12 - 0
backend/UnTube/settings/docker.py

@@ -0,0 +1,12 @@
+import os
+
+if IN_DOCKER or os.path.isfile('/.dockerenv'):  # type: ignore # noqa: F821
+    # We need it to serve static files with DEBUG=False
+    assert MIDDLEWARE[:1] == [  # type: ignore # noqa: F821
+        'django.middleware.security.SecurityMiddleware'
+    ]
+    MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')  # type: ignore # noqa: F821
+    DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+    STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+
+    print("Using Docker settings...")

+ 14 - 0
backend/UnTube/settings/envvars.py

@@ -0,0 +1,14 @@
+from backend.general.utils.collections import deep_update
+from backend.general.utils.settings import get_settings_from_environment
+
+"""
+This takes env variables with a matching prefix (set by you), strips out the prefix, and adds it to globals
+
+Eg. 
+export UNTUBE_SETTINGS_IN_DOCKER=true (environment variable)
+
+could be then referenced in the globals() dictionary as
+IN_DOCKER (where the value will be set to Pythonic True)
+"""
+# globals() is a dictionary of global variables
+deep_update(globals(), get_settings_from_environment(ENVVAR_SETTINGS_PREFIX))  # type: ignore

+ 48 - 0
backend/UnTube/settings/pythonanywhere.py

@@ -0,0 +1,48 @@
+if IN_PYTHONANYWHERE:  # type: ignore
+    # to use env variables on pythonanywhere
+    # from dotenv import load_dotenv
+    # project_folder = os.path.expanduser('/home/bakaabu')
+    # load_dotenv(os.path.join(project_folder, '.env'))
+    GOOGLE_OAUTH_URI = os.environ['GOOGLE_OAUTH_URI']  # type: ignore #  "bakaabu.pythonanywhere.com"
+    SECRET_KEY = os.environ['SECRET_KEY']  # type: ignore
+    YOUTUBE_V3_API_KEY = os.environ['YOUTUBE_V3_API_KEY']   # type: ignore
+
+    # WhiteNoise configuration
+    assert MIDDLEWARE[:1] == [  # type: ignore # noqa: F821
+        'django.middleware.security.SecurityMiddleware'
+    ] and not IN_DOCKER  # type: ignore # PA does not support dockerized apps
+    # Add whitenoise middleware after the security middleware
+    MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')  # type: ignore # noqa: F821
+
+    # configure the domain name using the environment variable found on pythonanywhere
+    ALLOWED_HOSTS = ['bakaabu.pythonanywhere.com', '127.0.0.1', 'untube.it']
+    SITE_ID = 10
+
+    CSRF_COOKIE_SECURE = True
+    SESSION_COOKIE_SECURE = True
+    SECURE_SSL_REDIRECT = True
+
+    STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')  # type: ignore
+
+    # DBHOST is only the server name
+    hostname = os.environ['DBHOST']  # type: ignore
+
+    # Configure MySQL database on pythonanywhere
+    # See https://django-mysql.readthedocs.io/en/latest/checks.html for options
+    DATABASES = {
+        'default': {
+            'ENGINE': 'django.db.backends.mysql',
+            'NAME': f'{os.environ["DBUSER"]}${os.environ["DBNAME"]}',  # type: ignore
+            'USER': f'{os.environ["DBUSER"]}',  # type: ignore
+            'PASSWORD': f'{os.environ["DBPASS"]}',  # type: ignore
+            'HOST': hostname,
+            'OPTIONS': {
+                'init_command': "SET sql_mode='STRICT_TRANS_TABLES', innodb_strict_mode=1",
+                'charset': 'utf8mb4',
+                "autocommit": True,
+            }
+        }
+    }
+
+    print("Using Pythonanywhere settings...")

+ 11 - 0
backend/UnTube/settings/templates/settings.dev.py

@@ -0,0 +1,11 @@
+DEBUG = True
+SECRET_KEY = "django-insecure-ycs22y+20sq67y(6dm6ynqw=dlhg!)%vuqpd@$p6rf3!#1h$u="
+ENABLE_PRINT_STATEMENTS = False
+
+SITE_ID = 2  # increment/decrement site ID as necessary
+
+# please fill these in with your own Google OAuth credentials for the app to run properly!
+YOUTUBE_V3_API_KEY = NotImplemented
+GOOGLE_OAUTH_URI = "127.0.0.1:8000"
+GOOGLE_OAUTH_CLIENT_ID = NotImplemented
+GOOGLE_OAUTH_CLIENT_SECRET = NotImplemented

+ 6 - 5
UnTube/urls.py → backend/UnTube/urls.py

@@ -18,9 +18,10 @@ from django.urls import path, include
 
 urlpatterns = [
     path('admin/', admin.site.urls),
-    path('', include("apps.users.urls")),
-    path('', include("apps.main.urls")),
-    path('manage/', include("apps.manage_playlists.urls")),
-    path('search/', include("apps.search.urls")),
-    path('charts/', include("apps.charts.urls")),
+    path('accounts/', include('allauth.urls')),
+    path('', include("backend.users.urls")),
+    path('', include("backend.main.urls")),
+    path('manage/', include("backend.manage_playlists.urls")),
+    path('search/', include("backend.search.urls")),
+    path('charts/', include("backend.charts.urls")),
 ]

+ 4 - 4
UnTube/wsgi.py → backend/UnTube/wsgi.py

@@ -8,14 +8,14 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
 """
 
 import os
-from dotenv import load_dotenv
 from django.core.wsgi import get_wsgi_application
 
-settings_module = "UnTube.production" if 'UNTUBE' in os.environ else 'UnTube.settings'
+settings_module = 'backend.UnTube.settings'
 os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
 
 # to use env variables on pythonanywhere
-project_folder = os.path.expanduser('/home/bakaabu')
-load_dotenv(os.path.join(project_folder, '.env'))
+# from dotenv import load_dotenv
+# project_folder = os.path.expanduser('/home/bakaabu')
+# load_dotenv(os.path.join(project_folder, '.env'))
 
 application = get_wsgi_application()

+ 0 - 0
apps/__init__.py → backend/__init__.py


+ 0 - 0
apps/charts/__init__.py → backend/charts/__init__.py


+ 0 - 0
apps/charts/admin.py → backend/charts/admin.py


+ 1 - 1
apps/charts/apps.py → backend/charts/apps.py

@@ -3,4 +3,4 @@ from django.apps import AppConfig
 
 class ChartsConfig(AppConfig):
     default_auto_field = 'django.db.models.BigAutoField'
-    name = 'apps.charts'
+    name = 'backend.charts'

+ 0 - 0
apps/charts/migrations/__init__.py → backend/charts/migrations/__init__.py


+ 0 - 0
apps/charts/models.py → backend/charts/models.py


+ 0 - 0
apps/charts/tests.py → backend/charts/tests.py


+ 1 - 1
apps/charts/urls.py → backend/charts/urls.py

@@ -1,5 +1,5 @@
 from django.urls import path
-from apps.charts import views
+from backend.charts import views
 
 urlpatterns = [
     path('channel-videos-distribution/<slug:playlist_id>', views.channel_videos_distribution, name='channel_videos_distribution'),

+ 0 - 0
apps/charts/views.py → backend/charts/views.py


+ 0 - 0
apps/main/__init__.py → backend/general/__init__.py


+ 0 - 0
apps/main/migrations/__init__.py → backend/general/utils/__init__.py


+ 20 - 0
backend/general/utils/collections.py

@@ -0,0 +1,20 @@
+def deep_update(base_dict, update_with):
+    """
+    Updates the base_dict (eg. settings/base.py settings) with the values present
+    in the update_with dict
+    """
+    # iterate over items in the update_with dict
+    for key, value in update_with.items():
+        if isinstance(value, dict):  # update_with dict value is a dict, i.e. update_with[key] = {}
+            base_dict_value = base_dict.get(key)
+
+            # check if base_dict value is also a dict
+            if isinstance(base_dict_value, dict):  # check if base_dict[key] = {}
+                deep_update(base_dict_value, value)  # recurse
+            else:
+                base_dict[key] = value  # else update the base dict with whatever dict value in update_with
+        else:
+            base_dict[key] = value
+
+    # return the updated base_dict
+    return base_dict

+ 20 - 0
backend/general/utils/misc.py

@@ -0,0 +1,20 @@
+import yaml
+from django.conf import settings
+
+
+def yaml_coerce(value):
+    """
+    Pass in a string dictionary and convert it into proper python dictionary
+    """
+    if isinstance(value, str):
+        # create a tiny snippet of YAML, with key=dummy, and val=value, then load the YAML into
+        # a pure Pythonic dictionary. Then, we read off the dummy key from the coversion to get our
+        # final result
+        return yaml.load(f'dummy: {value}', Loader=yaml.SafeLoader)['dummy']
+
+    return value
+
+
+def print_(*args, **kwargs):
+    if settings.ENABLE_PRINT_STATEMENTS:
+        print(*args, **kwargs)

+ 17 - 0
backend/general/utils/settings.py

@@ -0,0 +1,17 @@
+import os
+from .misc import yaml_coerce
+
+
+def get_settings_from_environment(prefix):
+    """
+    Django settings specific to the environment (eg. production) will be stored as environment variables
+    prefixed with "PREFIX_", eg. prefix="UNTUBESETTINGS_"
+    E.G. environment variables like UNTUBESETTINGS_SECRET_KEY=123, UNTUBESETTINGS_DATABASE="{'DB': {'NAME': 'db'}}"
+    will be converted to pure Python dictionary with the prefix "UNTUBESETTINGS_" removed from the keys
+    {
+       "SECRET_KEY": 123,
+       "DB": {'NAME': 'db'}
+    }
+    """
+    prefix_len = len(prefix)
+    return {key[prefix_len:]: yaml_coerce(value) for key, value in os.environ.items() if key.startswith(prefix)}

+ 0 - 0
apps/manage_playlists/__init__.py → backend/main/__init__.py


+ 0 - 0
apps/main/admin.py → backend/main/admin.py


+ 1 - 1
apps/main/apps.py → backend/main/apps.py

@@ -3,4 +3,4 @@ from django.apps import AppConfig
 
 class MainConfig(AppConfig):
     default_auto_field = 'django.db.models.BigAutoField'
-    name = 'apps.main'
+    name = 'backend.main'

+ 0 - 0
apps/main/migrations/0001_initial.py → backend/main/migrations/0001_initial.py


+ 0 - 0
apps/main/migrations/0002_auto_20211204_0521.py → backend/main/migrations/0002_auto_20211204_0521.py


+ 0 - 0
apps/manage_playlists/migrations/__init__.py → backend/main/migrations/__init__.py


+ 78 - 82
apps/main/models.py → backend/main/models.py

@@ -1,19 +1,11 @@
-import datetime
-
-import requests
 from django.contrib.auth.models import User
-from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
-from apps.users.models import Profile
 from .util import *
 import pytz
-from UnTube.secrets import SECRETS
 from django.db import models
-from google.oauth2.credentials import Credentials
-from google.auth.transport.requests import Request
-from datetime import timedelta
 from googleapiclient.discovery import build
 import googleapiclient.errors
 from django.db.models import Q, Sum
+from ..general.utils.misc import print_
 
 
 def get_message_from_httperror(e):
@@ -21,24 +13,24 @@ def get_message_from_httperror(e):
 
 
 class PlaylistManager(models.Manager):
-    def getCredentials(self, user):
-        app = SocialApp.objects.get(provider='google')
-        credentials = Credentials(
-            token=user.profile.access_token,
-            refresh_token=user.profile.refresh_token,
-            token_uri="https://oauth2.googleapis.com/token",
-            client_id=app.client_id,
-            client_secret=app.secret,
-            scopes=['https://www.googleapis.com/auth/youtube']
-        )
-
-        if not credentials.valid:
-            credentials.refresh(Request())
-            user.profile.access_token = credentials.token
-            user.profile.refresh_token = credentials.refresh_token
-            user.save()
-
-        return credentials
+    # def getCredentials(self, user):
+    #     app = SocialApp.objects.get(provider='google')
+    #     credentials = Credentials(
+    #         token=user.profile.access_token,
+    #         refresh_token=user.profile.refresh_token,
+    #         token_uri="https://oauth2.googleapis.com/token",
+    #         client_id=app.client_id,
+    #         client_secret=app.secret,
+    #         scopes=['https://www.googleapis.com/auth/youtube']
+    #     )
+    #
+    #     if not credentials.valid:
+    #         credentials.refresh(Request())
+    #         user.profile.access_token = credentials.token
+    #         user.profile.refresh_token = credentials.refresh_token
+    #         user.save()
+    #
+    #     return credentials
 
     def getPlaylistId(self, playlist_link):
         if "?" not in playlist_link:
@@ -53,7 +45,11 @@ class PlaylistManager(models.Manager):
     # Used to check if the user has a vaild YouTube channel
     # Will return -1 if user does not have a YouTube channel
     def getUserYTChannelID(self, user):
-        credentials = self.getCredentials(user)
+        user_profile = user.profile
+        if user_profile.yt_channel_id != "":
+            return 0
+
+        credentials = user_profile.get_credentials()
 
         with build('youtube', 'v3', credentials=credentials) as youtube:
             pl_request = youtube.channels().list(
@@ -63,14 +59,14 @@ class PlaylistManager(models.Manager):
 
             pl_response = pl_request.execute()
 
-            print(pl_response)
+            print_(pl_response)
 
             if pl_response['pageInfo']['totalResults'] == 0:
-                print("Looks like do not have a channel on youtube. Create one to import all of your playlists. Retry?")
+                print_("Looks like do not have a channel on youtube. Create one to import all of your playlists. Retry?")
                 return -1
             else:
-                user.profile.yt_channel_id = pl_response['items'][0]['id']
-                user.save()
+                user_profile.yt_channel_id = pl_response['items'][0]['id']
+                user_profile.save(update_fields=['yt_channel_id'])
 
         return 0
 
@@ -91,7 +87,7 @@ class PlaylistManager(models.Manager):
                   "error_message": "",
                   "playlist_ids": []}
 
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
         playlist_ids = []
         with build('youtube', 'v3', credentials=credentials) as youtube:
@@ -102,7 +98,7 @@ class PlaylistManager(models.Manager):
                     maxResults=50
                 )
             else:
-                print("GETTING ALL USER AUTH PLAYLISTS")
+                print_("GETTING ALL USER AUTH PLAYLISTS")
                 pl_request = youtube.playlists().list(
                     part='contentDetails, snippet, id, player, status',
                     mine=True,  # get playlist details for this playlist id
@@ -113,15 +109,15 @@ class PlaylistManager(models.Manager):
             try:
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError as e:
-                print("YouTube channel not found if mine=True")
-                print("YouTube playlist not found if id=playlist_id")
+                print_("YouTube channel not found if mine=True")
+                print_("YouTube playlist not found if id=playlist_id")
                 result["status"] = -1
                 result["error_message"] = get_message_from_httperror(e)
                 return result
 
-            print(pl_response)
+            print_(pl_response)
             if pl_response["pageInfo"]["totalResults"] == 0:
-                print("No playlists created yet on youtube.")
+                print_("No playlists created yet on youtube.")
                 result["status"] = -2
                 return result
 
@@ -150,7 +146,7 @@ class PlaylistManager(models.Manager):
             # check if this playlist already exists in user's untube collection
             if user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=True)).exists():
                 playlist = user.playlists.get(playlist_id=playlist_id)
-                print(f"PLAYLIST {playlist.name} ({playlist_id}) ALREADY EXISTS IN DB")
+                print_(f"PLAYLIST {playlist.name} ({playlist_id}) ALREADY EXISTS IN DB")
 
                 # POSSIBLE CASES:
                 # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
@@ -164,7 +160,7 @@ class PlaylistManager(models.Manager):
                     result["status"] = -3
                     return result
             else:  # no such playlist in database
-                print(f"CREATING {item['snippet']['title']} ({playlist_id})")
+                print_(f"CREATING {item['snippet']['title']} ({playlist_id})")
                 if user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=False)).exists():
                     unimported_playlist = user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=False)).first()
                     unimported_playlist.delete()
@@ -196,7 +192,7 @@ class PlaylistManager(models.Manager):
         return result
 
     def getAllVideosForPlaylist(self, user, playlist_id):
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
         playlist = user.playlists.get(playlist_id=playlist_id)
 
@@ -265,7 +261,7 @@ class PlaylistManager(models.Manager):
 
                 else:  # video found in user's db
                     if playlist.playlist_items.filter(playlist_item_id=playlist_item_id).exists():
-                        print("PLAYLIST ITEM ALREADY EXISTS")
+                        print_("PLAYLIST ITEM ALREADY EXISTS")
                         continue
 
                     video = user.videos.get(video_id=video_id)
@@ -463,14 +459,14 @@ class PlaylistManager(models.Manager):
         If full_scan is False, only the playlist count difference on YT and UT is checked on every visit
         to the playlist page. This is done everytime.
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
         playlist = user.playlists.get(playlist_id=pl_id)
 
         # if its been a week since the last full scan, do a full playlist scan
         # basically checks all the playlist video for any updates
         if playlist.last_full_scan_at + datetime.timedelta(minutes=1) < datetime.datetime.now(pytz.utc):
-            print("DOING A FULL SCAN")
+            print_("DOING A FULL SCAN")
             current_playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in
                                          playlist.playlist_items.all()]
 
@@ -551,10 +547,10 @@ class PlaylistManager(models.Manager):
 
             return [1, deleted_videos, unavailable_videos, added_videos]
         else:
-            print("YOU CAN DO A FULL SCAN AGAIN IN",
+            print_("YOU CAN DO A FULL SCAN AGAIN IN",
                   str(datetime.datetime.now(pytz.utc) - (playlist.last_full_scan_at + datetime.timedelta(minutes=1))))
         """
-        print("DOING A SMOL SCAN")
+        print_("DOING A SMOL SCAN")
 
         with build('youtube', 'v3', credentials=credentials) as youtube:
             pl_request = youtube.playlists().list(
@@ -567,11 +563,11 @@ class PlaylistManager(models.Manager):
             try:
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError:
-                print("YouTube channel not found if mine=True")
-                print("YouTube playlist not found if id=playlist_id")
+                print_("YouTube channel not found if mine=True")
+                print_("YouTube playlist not found if id=playlist_id")
                 return -1
 
-            print("PLAYLIST", pl_response)
+            print_("PLAYLIST", pl_response)
 
             playlist_items = []
 
@@ -593,7 +589,7 @@ class PlaylistManager(models.Manager):
             # check if this playlist already exists in database
             if user.playlists.filter(playlist_id=playlist_id).exists():
                 playlist = user.playlists.get(playlist_id__exact=playlist_id)
-                print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
+                print_(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
 
                 # POSSIBLE CASES:
                 # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
@@ -608,7 +604,7 @@ class PlaylistManager(models.Manager):
         return [0, "no change"]
 
     def updatePlaylist(self, user, playlist_id):
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
         playlist = user.playlists.get(playlist_id__exact=playlist_id)
 
@@ -632,10 +628,10 @@ class PlaylistManager(models.Manager):
             try:
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError:
-                print("Playist was deleted on YouTube")
+                print_("Playist was deleted on YouTube")
                 return [-1, [], [], []]
 
-            print("ESTIMATED VIDEO IDS FROM RESPONSE", len(pl_response["items"]))
+            print_("ESTIMATED VIDEO IDS FROM RESPONSE", len(pl_response["items"]))
             updated_playlist_video_count += len(pl_response["items"])
             for item in pl_response['items']:
                 playlist_item_id = item["id"]
@@ -914,7 +910,7 @@ class PlaylistManager(models.Manager):
         """
         Takes in playlist itemids for the videos in a particular playlist
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         playlist = user.playlists.get(playlist_id=playlist_id)
 
         # new_playlist_duration_in_seconds = playlist.playlist_duration_in_seconds
@@ -925,13 +921,13 @@ class PlaylistManager(models.Manager):
             )
             try:
                 pl_response = pl_request.execute()
-                print(pl_response)
+                print_(pl_response)
             except googleapiclient.errors.HttpError as e:  # failed to delete playlist
                 # possible causes:
                 # playlistForbidden (403)
                 # playlistNotFound  (404)
                 # playlistOperationUnsupported (400)
-                print(e.error_details, e.status_code)
+                print_(e.error_details, e.status_code)
                 return [-1, get_message_from_httperror(e), e.status_code]
 
             # playlistItem was successfully deleted if no HttpError, so delete it from db
@@ -948,7 +944,7 @@ class PlaylistManager(models.Manager):
         """
         Takes in playlist itemids for the videos in a particular playlist
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         playlist = user.playlists.get(playlist_id=playlist_id)
         playlist_items = user.playlists.get(playlist_id=playlist_id).playlist_items.select_related('video').filter(
             playlist_item_id__in=playlist_item_ids)
@@ -960,16 +956,16 @@ class PlaylistManager(models.Manager):
                 pl_request = youtube.playlistItems().delete(
                     id=playlist_item.playlist_item_id
                 )
-                print(pl_request)
+                print_(pl_request)
                 try:
                     pl_response = pl_request.execute()
-                    print(pl_response)
+                    print_(pl_response)
                 except googleapiclient.errors.HttpError as e:  # failed to delete playlist item
                     # possible causes:
                     # playlistItemsNotAccessible (403)
                     # playlistItemNotFound (404)
                     # playlistOperationUnsupported (400)
-                    print(e, e.error_details, e.status_code)
+                    print_(e, e.error_details, e.status_code)
                     continue
 
                 # playlistItem was successfully deleted if no HttpError, so delete it from db
@@ -1032,7 +1028,7 @@ class PlaylistManager(models.Manager):
         """
         Takes in playlist details and creates a new private playlist in the user's account
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         result = {
             "status": 0,
             "playlist_id": None
@@ -1054,7 +1050,7 @@ class PlaylistManager(models.Manager):
             try:
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError as e:  # failed to create playlist
-                print(e.status_code, e.error_details)
+                print_(e.status_code, e.error_details)
                 if e.status_code == 400:  # maxPlaylistExceeded
                     result["status"] = 400
                 result["status"] = -1
@@ -1066,7 +1062,7 @@ class PlaylistManager(models.Manager):
         """
         Takes in playlist itemids for the videos in a particular playlist
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         playlist = user.playlists.get(playlist_id=playlist_id)
 
         with build('youtube', 'v3', credentials=credentials) as youtube:
@@ -1084,7 +1080,7 @@ class PlaylistManager(models.Manager):
                 },
             )
 
-            print(details["description"])
+            print_(details["description"])
             try:
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError as e:  # failed to update playlist details
@@ -1094,10 +1090,10 @@ class PlaylistManager(models.Manager):
                 # playlistOperationUnsupported (400)
                 # errors i ran into:
                 # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
-                print("ERROR UPDATING PLAYLIST DETAILS", e, e.status_code, e.error_details)
+                print_("ERROR UPDATING PLAYLIST DETAILS", e, e.status_code, e.error_details)
                 return -1
 
-            print(pl_response)
+            print_(pl_response)
             playlist.name = pl_response['snippet']['title']
             playlist.description = pl_response['snippet']['description']
             playlist.is_private_on_yt = True if pl_response['status']['privacyStatus'] == "private" else False
@@ -1109,7 +1105,7 @@ class PlaylistManager(models.Manager):
         """
         Takes in playlist itemids for the videos in a particular playlist
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         playlist_items = user.playlists.get(playlist_id=from_playlist_id).playlist_items.select_related('video').filter(
             playlist_item_id__in=playlist_item_ids)
 
@@ -1145,7 +1141,7 @@ class PlaylistManager(models.Manager):
                         # playlistOperationUnsupported (400)
                         # errors i ran into:
                         # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
-                        print("ERROR UPDATING PLAYLIST DETAILS", e.status_code, e.error_details)
+                        print_("ERROR UPDATING PLAYLIST DETAILS", e.status_code, e.error_details)
                         if e.status_code == 400:
                             pl_request = youtube.playlistItems().insert(
                                 part="snippet",
@@ -1179,7 +1175,7 @@ class PlaylistManager(models.Manager):
         """
         Takes in playlist itemids for the videos in a particular playlist
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
         result = {
             "num_added": 0,
@@ -1207,7 +1203,7 @@ class PlaylistManager(models.Manager):
                 try:
                     pl_response = pl_request.execute()
                 except googleapiclient.errors.HttpError as e:  # failed to update add video to playlis
-                    print("ERROR ADDDING VIDEOS TO PLAYLIST", e.status_code, e.error_details)
+                    print_("ERROR ADDDING VIDEOS TO PLAYLIST", e.status_code, e.error_details)
                     if e.status_code == 400:  # manualSortRequired - see errors https://developers.google.com/youtube/v3/docs/playlistItems/insert
                         pl_request = youtube.playlistItems().insert(
                             part="snippet",
@@ -1398,18 +1394,18 @@ class Playlist(models.Model):
 
         return [num_channels, channels_list]
 
-    def generate_playlist_thumbnail_url(self):
-        """
-        Generates a playlist thumnail url based on the playlist name
-        """
-        pl_name = self.name
-        response = requests.get(
-            f'https://api.unsplash.com/search/photos/?client_id={SECRETS["UNSPLASH_API_ACCESS_KEY"]}&page=1&query={pl_name}')
-        image = response.json()["results"][0]["urls"]["small"]
-
-        print(image)
-
-        return image
+    # def generate_playlist_thumbnail_url(self):
+    #     """
+    #     Generates a playlist thumnail url based on the playlist name
+    #     """
+    #     pl_name = self.name
+    #     response = requests.get(
+    #         f'https://api.unsplash.com/search/photos/?client_id={SECRETS["UNSPLASH_API_ACCESS_KEY"]}&page=1&query={pl_name}')
+    #     image = response.json()["results"][0]["urls"]["small"]
+    #
+    #     print_(image)
+    #
+    #     return image
 
     def get_playlist_thumbnail_url(self):
         playlist_items = self.playlist_items.filter(

+ 0 - 0
apps/main/static/assets/imgs/dashboard.gif → backend/main/static/assets/imgs/dashboard.gif


+ 0 - 0
apps/main/static/assets/imgs/features.gif → backend/main/static/assets/imgs/features.gif


+ 0 - 0
apps/main/static/assets/imgs/import.gif → backend/main/static/assets/imgs/import.gif


+ 0 - 0
apps/main/static/assets/imgs/organize.gif → backend/main/static/assets/imgs/organize.gif


+ 0 - 0
apps/main/static/assets/imgs/playlist_stats.gif → backend/main/static/assets/imgs/playlist_stats.gif


+ 0 - 0
apps/main/static/assets/imgs/watching.gif → backend/main/static/assets/imgs/watching.gif


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.css → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.css → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.css → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.css → backend/main/static/bootstrap5.0.1/css/bootstrap.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.rtl.css → backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.rtl.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.bundle.js → backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.bundle.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js → backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.esm.js → backend/main/static/bootstrap5.0.1/js/bootstrap.esm.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.esm.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.esm.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js → backend/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.js → backend/main/static/bootstrap5.0.1/js/bootstrap.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.min.js → backend/main/static/bootstrap5.0.1/js/bootstrap.min.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.min.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.min.js.map


+ 0 - 0
apps/main/static/choices.js/choices.min.css → backend/main/static/choices.js/choices.min.css


+ 0 - 0
apps/main/static/choices.js/choices.min.js → backend/main/static/choices.js/choices.min.js


+ 0 - 0
apps/main/static/clipboard.js/clipboard.min.js → backend/main/static/clipboard.js/clipboard.min.js


+ 0 - 0
apps/main/static/css/carousel.css → backend/main/static/css/carousel.css


+ 0 - 0
apps/main/static/fontawesome-free-5.15.3-web/LICENSE.txt → backend/main/static/fontawesome-free-5.15.3-web/LICENSE.txt


+ 0 - 0
apps/main/static/fontawesome-free-5.15.3-web/attribution.js → backend/main/static/fontawesome-free-5.15.3-web/attribution.js


+ 6 - 6
apps/main/static/fontawesome-free-5.15.3-web/css/all.css → backend/main/static/fontawesome-free-5.15.3-web/css/all.css

@@ -4588,8 +4588,8 @@ readers do not read off random characters that represent icons */
   font-style: normal;
   font-weight: 400;
   font-display: block;
-  src: url("../webfonts/fa-brands-400.eot");
-  src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); }
+  src: url("fa-brands-400.eot");
+  src: url("fa-brands-400.eot?#iefix") format("embedded-opentype"), url("fa-brands-400.woff2") format("woff2"), url("fa-brands-400.woff") format("woff"), url("fa-brands-400.ttf") format("truetype"), url("fa-brands-400.svg#fontawesome") format("svg"); }
 
 .fab {
   font-family: 'Font Awesome 5 Brands';
@@ -4599,8 +4599,8 @@ readers do not read off random characters that represent icons */
   font-style: normal;
   font-weight: 400;
   font-display: block;
-  src: url("../webfonts/fa-regular-400.eot");
-  src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
+  src: url("fa-regular-400.eot");
+  src: url("fa-regular-400.eot?#iefix") format("embedded-opentype"), url("fa-regular-400.woff2") format("woff2"), url("fa-regular-400.woff") format("woff"), url("fa-regular-400.ttf") format("truetype"), url("fa-regular-400.svg#fontawesome") format("svg"); }
 
 .far {
   font-family: 'Font Awesome 5 Free';
@@ -4610,8 +4610,8 @@ readers do not read off random characters that represent icons */
   font-style: normal;
   font-weight: 900;
   font-display: block;
-  src: url("../webfonts/fa-solid-900.eot");
-  src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
+  src: url("fa-solid-900.eot");
+  src: url("fa-solid-900.eot?#iefix") format("embedded-opentype"), url("fa-solid-900.woff2") format("woff2"), url("fa-solid-900.woff") format("woff"), url("fa-solid-900.ttf") format("truetype"), url("fa-solid-900.svg#fontawesome") format("svg"); }
 
 .fa,
 .fas {

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
backend/main/static/fontawesome-free-5.15.3-web/css/all.min.css


+ 2 - 2
apps/main/static/fontawesome-free-5.15.3-web/css/brands.css → backend/main/static/fontawesome-free-5.15.3-web/css/brands.css

@@ -7,8 +7,8 @@
   font-style: normal;
   font-weight: 400;
   font-display: block;
-  src: url("../webfonts/fa-brands-400.eot");
-  src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); }
+  src: url("fa-brands-400.eot");
+  src: url("fa-brands-400.eot?#iefix") format("embedded-opentype"), url("fa-brands-400.woff2") format("woff2"), url("fa-brands-400.woff") format("woff"), url("fa-brands-400.ttf") format("truetype"), url("fa-brands-400.svg#fontawesome") format("svg"); }
 
 .fab {
   font-family: 'Font Awesome 5 Brands';

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor