From e082380c0ac30193958ef643c40cad7a439661d1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 26 Nov 2025 02:22:25 -0800 Subject: [PATCH 01/13] remove htmlminify and do not store huey results --- conreq/settings.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/conreq/settings.py b/conreq/settings.py index 0ee4038b..6b711e8a 100644 --- a/conreq/settings.py +++ b/conreq/settings.py @@ -82,8 +82,7 @@ SILKY_PYTHON_PROFILER = True SILKY_PYTHON_PROFILER_BINARY = True SILKY_PYTHON_PROFILER_RESULT_PATH = METRICS_DIR -HTML_MINIFY = True -WHITENOISE_MAX_AGE = 31536000 if not DEBUG else 0 +WHITENOISE_MAX_AGE = 0 if DEBUG else 31536000 COMPRESS_OUTPUT_DIR = "minified" COMPRESS_OFFLINE = True COMPRESS_STORAGE = "compressor.storage.BrotliCompressorFileStorage" @@ -96,7 +95,7 @@ "name": "huey", # DB name for huey. "huey_class": "huey.SqliteHuey", # Huey implementation to use. "filename": HUEY_FILENAME, # Sqlite filename - "results": True, # Store return values of tasks. + "results": False, # Whether to return values of tasks. "immediate": False, # If True, run tasks synchronously. "strict_fifo": True, # Utilize Sqlite AUTOINCREMENT to have unique task IDs "consumer": { @@ -296,8 +295,6 @@ "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", - "htmlmin.middleware.HtmlMinifyMiddleware", # Compresses HTML files - "htmlmin.middleware.MarkRequestMiddleware", # Marks the request as minified ] From a6e51b62de174ad11f1f7ee367157d3ae67dc149 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 26 Nov 2025 02:23:29 -0800 Subject: [PATCH 02/13] Add task expiration --- conreq/core/arrs/tasks.py | 4 ++-- conreq/core/base/tasks.py | 5 ++--- conreq/core/issue_reporting/tasks.py | 12 +++++------- conreq/core/user_requests/tasks.py | 4 ++-- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/conreq/core/arrs/tasks.py b/conreq/core/arrs/tasks.py index a939722a..75decf85 100644 --- a/conreq/core/arrs/tasks.py +++ b/conreq/core/arrs/tasks.py @@ -9,13 +9,13 @@ ARR_REFRESH_INTERNAL = get_str_from_env("ARR_REFRESH_INTERNAL", "*/1") -@db_periodic_task(crontab(minute=ARR_REFRESH_INTERNAL, strict=True)) +@db_periodic_task(crontab(minute=ARR_REFRESH_INTERNAL, strict=True), expires=120) def refresh_radarr_library(): """Checks Radarr for new entries.""" RadarrManager().refresh_library() -@db_periodic_task(crontab(minute=ARR_REFRESH_INTERNAL, strict=True)) +@db_periodic_task(crontab(minute=ARR_REFRESH_INTERNAL, strict=True), expires=120) def refresh_sonarr_library(): """Checks Sonarr for new entries.""" SonarrManager().refresh_library() diff --git a/conreq/core/base/tasks.py b/conreq/core/base/tasks.py index ee7803de..1ec4d7f3 100644 --- a/conreq/core/base/tasks.py +++ b/conreq/core/base/tasks.py @@ -1,4 +1,3 @@ -import os import sqlite3 from django.conf import settings @@ -12,7 +11,7 @@ HUEY_FILENAME = getattr(settings, "HUEY_FILENAME") -@db_periodic_task(crontab(minute="0", hour="0", strict=True)) +@db_periodic_task(crontab(minute="0", hour="0", strict=True), expires=120) def vacuum_huey_sqlite_db(): """Periodically preforms a SQLITE vacuum on the background task database.""" with sqlite3.connect(HUEY_FILENAME) as cursor: @@ -36,7 +35,7 @@ def vacuum_huey_sqlite_db(): if DB_ENGINE == "SQLITE3": - @db_periodic_task(crontab(minute="0", hour="0", strict=True)) + @db_periodic_task(crontab(minute="0", hour="0", strict=True), expires=120) def vacuum_conreq_sqlite_db(): """Periodically performs any cleanup tasks needed for the default database.""" with connection.cursor() as cursor: diff --git a/conreq/core/issue_reporting/tasks.py b/conreq/core/issue_reporting/tasks.py index e947bd05..e00f9313 100644 --- a/conreq/core/issue_reporting/tasks.py +++ b/conreq/core/issue_reporting/tasks.py @@ -14,7 +14,7 @@ from .models import ReportedIssue -@db_task() +@db_task(expires=120) def arr_auto_resolve_tv(issue_id, tmdb_id, seasons, episode_ids, resolutions): """Queues a background task to automatically resolve a reported issue.""" # TODO: Intelligently resolve based on "resolutions" @@ -64,12 +64,10 @@ def arr_auto_resolve_tv(issue_id, tmdb_id, seasons, episode_ids, resolutions): for episode in season.get("episodes"): if ( # User reported an episode, check if the episode has a file - episode.get("id") in episode_ids - and episode.get("hasFile") + episode.get("id") in episode_ids and episode.get("hasFile") ) or ( # User reported a season, check if the season has episode files to be deleted - season.get("seasonNumber") in seasons - and episode.get("hasFile") + season.get("seasonNumber") in seasons and episode.get("hasFile") ): sonarr_manager.delete_episode( episode_file_id=episode.get("episodeFileId") @@ -87,7 +85,7 @@ def arr_auto_resolve_tv(issue_id, tmdb_id, seasons, episode_ids, resolutions): issue.save() -@db_task() +@db_task(expires=120) def arr_auto_resolve_movie(issue_id, tmdb_id, resolutions): """Queues a background task to automatically resolve a reported issue.""" # TODO: Intelligently resolve based on "resolutions" @@ -118,7 +116,7 @@ def arr_auto_resolve_movie(issue_id, tmdb_id, resolutions): issue.save() -@db_periodic_task(crontab(minute="*/1", strict=True)) +@db_periodic_task(crontab(minute="*/1", strict=True), expires=120) def auto_resolve_completion_watchdog(): """Checks to see if an auto resolution has finished completely.""" # Check if auto resolution is turned on diff --git a/conreq/core/user_requests/tasks.py b/conreq/core/user_requests/tasks.py index 4219f223..d82c00fe 100644 --- a/conreq/core/user_requests/tasks.py +++ b/conreq/core/user_requests/tasks.py @@ -7,7 +7,7 @@ _logger = log.get_logger(__name__) -@db_task() +@db_task(expires=120) def sonarr_request_background_task(tvdb_id, request_params, sonarr_params): """Function that can be run in the background to request something on Sonarr""" try: @@ -55,7 +55,7 @@ def sonarr_request_background_task(tvdb_id, request_params, sonarr_params): ) -@db_task() +@db_task(expires=120) def radarr_request_background_task(tmdb_id, radarr_params): """Function that can be run in the background to request something on Radarr""" try: From 49a6d3a9854bf7e6a73c6f2bd67b7d3fb97547c0 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 26 Nov 2025 02:32:25 -0800 Subject: [PATCH 03/13] remove htmlmin dep --- requirements/main.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements/main.txt b/requirements/main.txt index 6922f937..1c9c768a 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -4,7 +4,6 @@ django==3.2.25 django-cleanup==8.1.0 django-compression-middleware==0.5.0 django-compressor==4.4 -django-htmlmin==0.11.0 django-model-utils==4.5.1 django-searchable-encrypted-fields==0.2.1 django-silk==5.1.0 From a3d827a4cacc99db7e6551c4fa97f0e748e81071 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 26 Nov 2025 03:07:48 -0800 Subject: [PATCH 04/13] Bump dependencies and remove undeveloped API --- conreq/asgi.py | 1 + conreq/core/api/__init__.py | 0 conreq/core/api/admin.py | 5 - conreq/core/api/apps.py | 6 - conreq/core/api/migrations/__init__.py | 0 conreq/core/api/models.py | 15 -- conreq/core/api/permissions.py | 18 -- conreq/core/api/serializers.py | 33 --- conreq/core/api/urls.py | 54 ----- conreq/core/api/views.py | 189 ------------------ .../core/base/templates/primary/sidebar.html | 12 +- conreq/core/base/validators.py | 25 +-- conreq/settings.py | 23 +-- conreq/urls.py | 47 ----- requirements/main.txt | 39 ++-- 15 files changed, 36 insertions(+), 431 deletions(-) delete mode 100644 conreq/core/api/__init__.py delete mode 100644 conreq/core/api/admin.py delete mode 100644 conreq/core/api/apps.py delete mode 100644 conreq/core/api/migrations/__init__.py delete mode 100644 conreq/core/api/models.py delete mode 100644 conreq/core/api/permissions.py delete mode 100644 conreq/core/api/serializers.py delete mode 100644 conreq/core/api/urls.py delete mode 100644 conreq/core/api/views.py diff --git a/conreq/asgi.py b/conreq/asgi.py index 208a2cf2..8aa85d44 100644 --- a/conreq/asgi.py +++ b/conreq/asgi.py @@ -6,6 +6,7 @@ For more information on this file, see https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ """ + # pylint: disable=wrong-import-position from django.core.asgi import get_asgi_application from django.urls import path diff --git a/conreq/core/api/__init__.py b/conreq/core/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/conreq/core/api/admin.py b/conreq/core/api/admin.py deleted file mode 100644 index d5cd1133..00000000 --- a/conreq/core/api/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from rest_framework.authtoken.admin import TokenAdmin - -# Register your models here. - -TokenAdmin.raw_id_fields = ["user"] diff --git a/conreq/core/api/apps.py b/conreq/core/api/apps.py deleted file mode 100644 index 59903295..00000000 --- a/conreq/core/api/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class ApiConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "conreq.core.api" diff --git a/conreq/core/api/migrations/__init__.py b/conreq/core/api/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/conreq/core/api/models.py b/conreq/core/api/models.py deleted file mode 100644 index f98221ca..00000000 --- a/conreq/core/api/models.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.contrib.auth import get_user_model -from django.db.models.signals import post_save -from django.dispatch import receiver -from rest_framework.authtoken.models import Token - -User = get_user_model() - -# Create your models here. -@receiver(post_save, sender=User) -def create_auth_token( - sender, instance=None, created=False, **kwargs -): # pylint: disable=unused-argument - # Create the token if it doesn't exist - if not hasattr(instance, "auth_token"): - Token.objects.create(user=instance) diff --git a/conreq/core/api/permissions.py b/conreq/core/api/permissions.py deleted file mode 100644 index 5bbfe33b..00000000 --- a/conreq/core/api/permissions.py +++ /dev/null @@ -1,18 +0,0 @@ -import typing - -from django.http import HttpRequest -from rest_framework_api_key.models import APIKey -from rest_framework_api_key.permissions import BaseHasAPIKey, KeyParser - - -class HasAPIKey(BaseHasAPIKey): - model = APIKey - key_parser = KeyParser() - - def get_key(self, request: HttpRequest) -> typing.Optional[str]: - # Prefer key in header - header_key = self.key_parser.get(request) - if header_key: - return header_key - # Fallback to key in URL parameters - return request.GET.get("apikey") diff --git a/conreq/core/api/serializers.py b/conreq/core/api/serializers.py deleted file mode 100644 index 85d43391..00000000 --- a/conreq/core/api/serializers.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.contrib.auth import get_user_model -from rest_framework import serializers - -from conreq.core.manage_users.models import Profile - - -class UserProfileSerializer(serializers.ModelSerializer): - class Meta: - model = Profile - fields = ["language", "externally_authenticated"] - - -class UserSerializer(serializers.ModelSerializer): - profile = UserProfileSerializer(required=True) - - class Meta: - model = get_user_model() - fields = [ - "id", - "last_login", - "is_superuser", - "username", - "first_name", - "last_name", - "email", - "is_staff", - "is_active", - "date_joined", - "groups", - "user_permissions", - "profile", - "auth_token", - ] diff --git a/conreq/core/api/urls.py b/conreq/core/api/urls.py deleted file mode 100644 index 930916ec..00000000 --- a/conreq/core/api/urls.py +++ /dev/null @@ -1,54 +0,0 @@ -from django.urls import path -from rest_framework.authtoken.views import ObtainAuthToken - -from . import views - -urlpatterns = [ - # Default Endpoints - path("user", views.stub), # GET/PUT - path("user/me", views.stub), # GET/PUT - path("user/", views.stub), # GET/PUT/DELETE - path("auth/local", views.LocalAuthentication.as_view()), # POST - path("auth/local/token", ObtainAuthToken.as_view()), # POST/DELETE - path("system", views.stub), # GET - path("system/settings", views.stub), # GET/POST - # TMDB Requests App - path("tmdb/request", views.stub), # GET - path("tmdb/request/", views.stub), # GET/DELETE - path("tmdb/request/tv/", views.RequestTv.as_view()), # POST - path("tmdb/request/movie/", views.RequestMovie.as_view()), # POST - path("tmdb/issue", views.stub), # GET/POST - path("tmdb/issue/", views.stub), # GET/DELETE - path("user//tmdb/issues", views.stub), # GET - path("user//tmdb/requests", views.stub), # GET - # Sonarr/Radarr Connection App - path("sonarr/library", views.stub), # GET - path("sonarr/settings", views.stub), # GET/POST - path("radarr/library", views.stub), # GET - path("radarr/settings", views.stub), # GET/POST - path("system/settings/sonarr", views.stub), # GET/POST - path("system/settings/radarr", views.stub), # GET/POST - # TMDB Connection App - path("tmdb/discover", views.stub), # GET - path("tmdb/discover/tv", views.stub), # GET - path("tmdb/discover/tv/filters", views.stub), # GET - path("tmdb/discover/tv/filters/", views.stub), # GET - path("tmdb/discover/movies", views.stub), # GET - path("tmdb/discover/movies/filters", views.stub), # GET - path("tmdb/discover/movies/filters/", views.stub), # GET - path("tmdb/search", views.stub), # GET - path("tmdb/search/tv", views.stub), # GET - path("tmdb/search/movie", views.stub), # GET - path("tmdb/tv/", views.stub), # GET - path("tmdb/tv//recommendations", views.stub), # GET - path("tmdb/tv/genres", views.stub), # GET - path("tmdb/movie/", views.stub), # GET - path("tmdb/movie//recommendations", views.stub), # GET - path("tmdb/movie/genres", views.stub), # GET - path("tmdb/person/", views.stub), # GET - path("tmdb/regions", views.stub), # GET - path("tmdb/languages", views.stub), # GET - path("tmdb/studio/", views.stub), # GET - path("tmdb/network/", views.stub), # GET - path("tmdb/collection/", views.stub), # GET -] diff --git a/conreq/core/api/views.py b/conreq/core/api/views.py deleted file mode 100644 index 6512e1a5..00000000 --- a/conreq/core/api/views.py +++ /dev/null @@ -1,189 +0,0 @@ -import json - -from django.contrib.auth import authenticate, login -from drf_yasg import openapi -from drf_yasg.utils import swagger_auto_schema -from rest_framework import status -from rest_framework.decorators import api_view -from rest_framework.response import Response -from rest_framework.views import APIView - -from conreq.core.tmdb.discovery import TmdbDiscovery -from conreq.core.user_requests.helpers import radarr_request, sonarr_request - -from .serializers import UserSerializer - - -# Create your views here. -class RequestTv(APIView): - """Request a TV show by TMDB ID.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.msg = {"success": True, "detail": None} - - @swagger_auto_schema( - request_body=openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - "seasons": openapi.Schema( - type=openapi.TYPE_ARRAY, - items=openapi.Items(type=openapi.TYPE_INTEGER), - ), - "episodes": openapi.Schema( - type=openapi.TYPE_ARRAY, - items=openapi.Items(type=openapi.TYPE_INTEGER), - ), - }, - ), - responses={ - status.HTTP_200_OK: openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - "success": openapi.Schema( - type=openapi.TYPE_BOOLEAN, - ), - "detail": openapi.Schema( - type=openapi.TYPE_STRING, - ), - }, - ), - }, - ) - def post(self, request, tmdb_id): - """Request a TV show by TMDB ID. Optionally, you can request specific seasons or episodes.""" - tvdb_id = TmdbDiscovery().get_external_ids(tmdb_id, "tv") - request_parameters = json.loads(request.body.decode("utf-8")) - - # Request the show by the TVDB ID - if tvdb_id: - sonarr_request( - tvdb_id["tvdb_id"], - tmdb_id, - request, - request_parameters, - ) - return Response(self.msg) - return Response({"success": False, "detail": "Could not determine TVDB ID."}) - - -class RequestMovie(APIView): - """Request a movie by TMDB ID.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.msg = {"success": True, "detail": None} - - @swagger_auto_schema( - responses={ - status.HTTP_200_OK: openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - "success": openapi.Schema( - type=openapi.TYPE_BOOLEAN, - ), - "detail": openapi.Schema( - type=openapi.TYPE_STRING, - ), - }, - ), - }, - ) - def post(self, request, tmdb_id): - """Request a movie by TMDB ID.""" - - # Request the show by the TMDB ID - radarr_request(tmdb_id, request) - return Response(self.msg) - - -class LocalAuthentication(APIView): - """Sign in to an account.""" - - @swagger_auto_schema( - request_body=openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - "username": openapi.Schema( - type=openapi.TYPE_STRING, - ), - "password": openapi.Schema( - type=openapi.TYPE_STRING, - ), - }, - ), - responses={ - status.HTTP_200_OK: openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - "id": openapi.Schema( - type=openapi.TYPE_INTEGER, - ), - "last_login": openapi.Schema( - type=openapi.TYPE_STRING, - ), - "is_superuser": openapi.Schema( - type=openapi.TYPE_BOOLEAN, - ), - "username": openapi.Schema( - type=openapi.TYPE_STRING, - ), - "first_name": openapi.Schema( - type=openapi.TYPE_STRING, - ), - "last_name": openapi.Schema( - type=openapi.TYPE_STRING, - ), - "email": openapi.Schema( - type=openapi.TYPE_STRING, - ), - "is_staff": openapi.Schema( - type=openapi.TYPE_BOOLEAN, - ), - "is_active": openapi.Schema( - type=openapi.TYPE_BOOLEAN, - ), - "date_joined": openapi.Schema( - type=openapi.TYPE_STRING, - ), - "groups": openapi.Schema( - type=openapi.TYPE_ARRAY, - items=openapi.Items(type=openapi.TYPE_INTEGER), - ), - "user_permissions": openapi.Schema( - type=openapi.TYPE_ARRAY, - items=openapi.Items(type=openapi.TYPE_INTEGER), - ), - "profile": openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - "language": openapi.Schema( - type=openapi.TYPE_STRING, - ), - "externally_authenticated": openapi.Schema( - type=openapi.TYPE_BOOLEAN, - ), - }, - ), - "auth_token": openapi.Schema( - type=openapi.TYPE_STRING, - ), - }, - ), - }, - ) - def post(self, request): - """Authenticate a session using a `username` and `password`. - Requires CSRF tokens on all further insecure requests (POST, PUT, DELETE, PATCH).""" - username = request.data.get("username") - password = request.data.get("password") - user = authenticate(request, username=username, password=password) - if user is not None: - login(request, user) - return Response(UserSerializer(user).data) - - -@api_view(["GET"]) -def stub(request): - """This is a stub for an endpoint that has not yet been developed.""" - return Response({}) diff --git a/conreq/core/base/templates/primary/sidebar.html b/conreq/core/base/templates/primary/sidebar.html index b540f91b..d2498739 100644 --- a/conreq/core/base/templates/primary/sidebar.html +++ b/conreq/core/base/templates/primary/sidebar.html @@ -166,18 +166,8 @@ Code Outline - - {% endif %} - \ No newline at end of file + diff --git a/conreq/core/base/validators.py b/conreq/core/base/validators.py index b5270bff..0ea75c37 100644 --- a/conreq/core/base/validators.py +++ b/conreq/core/base/validators.py @@ -9,29 +9,30 @@ class ExtendedURLValidator(URLValidator): """URL validator that supports hostnames (ex. https://sonarr:8000)""" - # pylint: disable=too-few-public-methods - ul = URLValidator.ul + # IP patterns ipv4_re = URLValidator.ipv4_re ipv6_re = URLValidator.ipv6_re hostname_re = URLValidator.hostname_re domain_re = URLValidator.domain_re + tld_re = URLValidator.tld_re - tld_re = ( - r"\.?" # OPTIONAL dot (allows for hostnames) - r"(?!-)" # can't start with a dash - r"(?:[a-z" + ul + "-]{2,63}" # domain label - r"|xn--[a-z0-9]{1,59})" # or punycode label - r"(? settings.env SETTINGS_FILE = os.path.join(DATA_DIR, "settings.json") if os.path.exists(SETTINGS_FILE): @@ -269,7 +257,7 @@ "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", - "whitenoise.runserver_nostatic", + "servestatic.runserver_nostatic", "django.contrib.staticfiles", *list_modules(CORE_DIR, prefix="conreq.core."), "encrypted_fields", # Allow for encrypted text in the DB @@ -278,15 +266,12 @@ "huey.contrib.djhuey", # Queuing background tasks "compressor", # Minifies CSS/JS files "url_or_relative_url_field", # Validates relative URLs - "rest_framework", # OpenAPI Framework - "rest_framework_api_key", # API Key Manager - "rest_framework.authtoken", # API User Authentication *list_modules(APPS_DIR), # User Installed Apps ] MIDDLEWARE = [ "compression_middleware.middleware.CompressionMiddleware", "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", # Serve static files through Django securely + "servestatic.middleware.ServeStaticMiddleware", # Serve static files through Django securely "django.middleware.gzip.GZipMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.http.ConditionalGetMiddleware", @@ -303,8 +288,6 @@ # Performance analysis tools INSTALLED_APPS.append("silk") MIDDLEWARE.append("silk.middleware.SilkyMiddleware") - # API docs generator - INSTALLED_APPS.append("drf_yasg") # URL Routing and Page Rendering diff --git a/conreq/urls.py b/conreq/urls.py index bc603543..b05e288f 100644 --- a/conreq/urls.py +++ b/conreq/urls.py @@ -47,7 +47,6 @@ path("search/", include("conreq.core.search.urls")), path("manage_users/", include("conreq.core.manage_users.urls")), path("server_settings/", include("conreq.core.server_settings.urls")), - path("api/v1/", include("conreq.core.api.urls")), ] # Add User Installed Apps URLS @@ -62,52 +61,6 @@ urlpatterns.append(path("admin/docs/", include("django.contrib.admindocs.urls"))) urlpatterns.append(path("admin/", admin.site.urls)) - # Django Rest Framework documentation (Swagger and Redoc) - # pylint: disable=ungrouped-imports - from django.urls import re_path - from drf_yasg import openapi - from drf_yasg.views import get_schema_view - from rest_framework import permissions - - SchemaView = get_schema_view( - openapi.Info( - title="Conreq API Endpoints", - default_version="v1", - description=""" - Outline for all endpoints available within this Conreq instance. - - All endpoints require an API key either in **HTTP Header (Authorization: Api-Key)** or in the **URL Parameter (apikey)**. - - Token Authentication is performed using **HTTP Header (Authorization: Token)**. Session Authentication can alternatively be performed. - """, - contact=openapi.Contact(email="archiethemonger@gmail.com"), - license=openapi.License(name="GPL-3.0 License"), - ), - public=True, - permission_classes=[permissions.AllowAny], - ) - - docs_urlpatterns = [ - re_path( - r"^swagger(?P\.json|\.yaml)$", - SchemaView.without_ui(cache_timeout=0), - name="schema-json", - ), - re_path( - r"^swagger/$", - SchemaView.with_ui("swagger", cache_timeout=0), - name="schema-swagger-ui", - ), - re_path( - r"^redoc/$", - SchemaView.with_ui("redoc", cache_timeout=0), - name="schema-redoc", - ), - ] - - for pattern in docs_urlpatterns: - urlpatterns.append(pattern) - # Wrap the urlpatterns in BASE_URL if required if BASE_URL: diff --git a/requirements/main.txt b/requirements/main.txt index 1c9c768a..ca7cea7f 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -1,30 +1,27 @@ -channels[daphne]==4.0.0 +channels[daphne]==4.3.2 diskcache==5.6.3 -django==3.2.25 -django-cleanup==8.1.0 +django==5.2.8 +django-cleanup==9.0.0 django-compression-middleware==0.5.0 -django-compressor==4.4 -django-model-utils==4.5.1 +django-compressor==4.6 +django-model-utils==5.0.0 django-searchable-encrypted-fields==0.2.1 -django-silk==5.1.0 -django-solo==2.2.0 +django-silk==5.4.3 +django-solo==2.4.0 django-url-or-relative-url-field==0.2.0 -djangorestframework==3.15.1 -djangorestframework-api-key==3.0.0 -docutils==0.21.2 -drf-yasg==1.21.7 -huey==2.5.1 -hypercorn[h3]==0.17.3 -jsonfield==3.1.0 -markdown==3.6.0 -pwned-passwords-django==2.1 +huey==2.5.4 +hypercorn[h3]==0.18.0 +jsonfield==3.2.0 +markdown==3.10.0 +pwned-passwords-django==5.2.0 pyarr==5.2.0 -pymysql==1.1.1 -python-dotenv==1.0.1 +pymysql==1.1.2 +python-dotenv==1.2.1 strsim==0.0.3 titlecase==2.4.1 tmdbsimple==2.9.1 -Twisted[tls,http2]==24.3.0 -tzlocal==5.2 -whitenoise[brotli]==6.7.0 +Twisted[tls,http2]==25.5.0 +tzlocal==5.3.1 +servestatic[brotli]==3.1.0 attrs +cffi From 4ca6dfa14b0419ae908a48069f6f1a670bee0eda Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 26 Nov 2025 03:25:11 -0800 Subject: [PATCH 05/13] Remove `diskcache` usage --- conreq/core/tmdb/discovery.py | 1 + conreq/settings.py | 6 +----- requirements/main.txt | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/conreq/core/tmdb/discovery.py b/conreq/core/tmdb/discovery.py index cf9de180..1ae689e9 100644 --- a/conreq/core/tmdb/discovery.py +++ b/conreq/core/tmdb/discovery.py @@ -1,4 +1,5 @@ """Conreq Content Discovery: Searches TMDB for content.""" + import tmdbsimple as tmdb from conreq.utils import cache, log diff --git a/conreq/settings.py b/conreq/settings.py index 12079ad6..380451af 100644 --- a/conreq/settings.py +++ b/conreq/settings.py @@ -344,12 +344,8 @@ } CACHES = { "default": { - "BACKEND": "diskcache.DjangoCache", + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", "LOCATION": os.path.join(DATA_DIR, "cache"), - "TIMEOUT": 300, # Django setting for default timeout of each key. - "SHARDS": 8, # Number of "sharded" cache dbs to create - "DATABASE_TIMEOUT": 0.25, # 250 milliseconds - "OPTIONS": {"size_limit": 2**30}, # 1 gigabyte } } diff --git a/requirements/main.txt b/requirements/main.txt index ca7cea7f..f081d5de 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -1,5 +1,4 @@ channels[daphne]==4.3.2 -diskcache==5.6.3 django==5.2.8 django-cleanup==9.0.0 django-compression-middleware==0.5.0 From 6928f6d348e76bea52a50a318221bce1a36c3332 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:03:41 -0800 Subject: [PATCH 06/13] better db maintenence tasks --- .../base/management/commands/preconfig_conreq.py | 8 +++++--- conreq/core/base/tasks.py | 14 ++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/conreq/core/base/management/commands/preconfig_conreq.py b/conreq/core/base/management/commands/preconfig_conreq.py index 12124cc0..3f0e77c8 100644 --- a/conreq/core/base/management/commands/preconfig_conreq.py +++ b/conreq/core/base/management/commands/preconfig_conreq.py @@ -79,10 +79,12 @@ def setup_sqlite_database(path, name, uid, gid, no_perms): if not os.path.exists(path): print("> Creating database") with sqlite3.connect(path) as cursor: + print("> Optimizing database") + cursor.execute("PRAGMA optimize;") print("> Vacuuming database") - cursor.execute("VACUUM") - print("> Configuring database") - cursor.execute("PRAGMA journal_mode = WAL;") + cursor.execute("VACUUM;") + print("> Reindexing database") + cursor.execute("REINDEX;") if not no_perms and (uid != -1 or gid != -1) and sys.platform == "linux": # pylint: disable=no-member print("> Applying permissions") diff --git a/conreq/core/base/tasks.py b/conreq/core/base/tasks.py index 1ec4d7f3..b2eea7cd 100644 --- a/conreq/core/base/tasks.py +++ b/conreq/core/base/tasks.py @@ -12,8 +12,7 @@ @db_periodic_task(crontab(minute="0", hour="0", strict=True), expires=120) -def vacuum_huey_sqlite_db(): - """Periodically preforms a SQLITE vacuum on the background task database.""" +def huey_db_maintenance(): with sqlite3.connect(HUEY_FILENAME) as cursor: cursor.execute( # Only keep the 1000 latest tasks @@ -30,13 +29,16 @@ def vacuum_huey_sqlite_db(): """ ) with sqlite3.connect(HUEY_FILENAME) as cursor: - cursor.execute("VACUUM") + cursor.execute("PRAGMA optimize;") + cursor.execute("VACUUM;") + cursor.execute("REINDEX;") if DB_ENGINE == "SQLITE3": @db_periodic_task(crontab(minute="0", hour="0", strict=True), expires=120) - def vacuum_conreq_sqlite_db(): - """Periodically performs any cleanup tasks needed for the default database.""" + def conreq_db_maintenance(): with connection.cursor() as cursor: - cursor.execute("VACUUM") + cursor.execute("PRAGMA optimize;") + cursor.execute("VACUUM;") + cursor.execute("REINDEX;") From fba89a06339d5c8ffc55ffa9c22bdd31126f75a7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:05:16 -0800 Subject: [PATCH 07/13] add process supervisor for webserver and huey --- conreq/asgi.py | 3 - .../base/management/commands/run_conreq.py | 113 ++++++++++-------- requirements/main.txt | 2 +- 3 files changed, 66 insertions(+), 52 deletions(-) diff --git a/conreq/asgi.py b/conreq/asgi.py index 8aa85d44..0e71e18f 100644 --- a/conreq/asgi.py +++ b/conreq/asgi.py @@ -2,9 +2,6 @@ ASGI config for Conreq project. It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ """ # pylint: disable=wrong-import-position diff --git a/conreq/core/base/management/commands/run_conreq.py b/conreq/core/base/management/commands/run_conreq.py index 3361d3ab..4dd363e6 100644 --- a/conreq/core/base/management/commands/run_conreq.py +++ b/conreq/core/base/management/commands/run_conreq.py @@ -1,22 +1,25 @@ +import contextlib import os -import sqlite3 +from logging import getLogger from multiprocessing import Process +from time import sleep import django +import uvicorn from django.conf import settings from django.core.cache import cache from django.core.management import call_command from django.core.management.base import BaseCommand -from hypercorn.config import Config as HypercornConfig -from hypercorn.run import run as run_hypercorn from conreq.utils.environment import get_debug -HYPERCORN_TOML = os.path.join(getattr(settings, "DATA_DIR"), "hypercorn.toml") +UVICORN_CONFIG = os.path.join(getattr(settings, "DATA_DIR"), "uvicorn.env") DEBUG = get_debug() HUEY_FILENAME = getattr(settings, "HUEY_FILENAME") ACCESS_LOG_FILE = getattr(settings, "ACCESS_LOG_FILE") +_logger = getLogger(__name__) + class Command(BaseCommand): help = "Runs all commands needed to safely start Conreq." @@ -25,6 +28,13 @@ def handle(self, *args, **options): port = options["port"] verbosity = "-v 1" if DEBUG else "-v 0" + # Perform clean-up + print("Removing stale background tasks...") + self.reset_huey_db() + if DEBUG: + print("Clearing cache...") + cache.clear() + # Run any preconfiguration tasks if not options["disable_preconfig"]: preconfig_args = [ @@ -40,45 +50,44 @@ def handle(self, *args, **options): if not options["skip_checks"]: call_command("test", "--noinput", "--parallel", "--failfast") - # Perform any debug related clean-up - if DEBUG: - print("Conreq is in DEBUG mode.") - print("Clearing cache...") - cache.clear() - print("Removing stale background tasks...") - self.reset_huey_db() - # Migrate the database call_command("migrate", "--noinput", verbosity) + # Collect static files if not DEBUG: - # Collect static files call_command("collectstatic", "--link", "--clear", "--noinput", verbosity) call_command("compress", "--force", verbosity) - # Run background task management - proc = Process(target=self.start_huey, daemon=True) - proc.start() + huey = Process(target=start_huey) + huey.start() + webserver = Process(target=start_webserver, kwargs={"port": port}) + webserver.start() - # Run the production webserver - if not DEBUG: - config = HypercornConfig() - config.bind = f"0.0.0.0:{port}" - config.websocket_ping_interval = 20 - config.workers = 3 - config.application_path = "conreq.asgi:application" - config.accesslog = ACCESS_LOG_FILE + while True: + if not huey.is_alive(): + _logger.warning("Background task manager has crashed. Restarting...") + huey = Process(target=start_huey, daemon=True) + huey.start() - # Additonal webserver configuration - if os.path.exists(HYPERCORN_TOML): - config.from_toml(HYPERCORN_TOML) + if not webserver.is_alive(): + _logger.warning("Webserver has crashed. Restarting...") + webserver = Process(target=start_webserver, kwargs={"port": port}) + webserver.start() - # Run the webserver - run_hypercorn(config) + sleep(5) - # Run the development webserver - if DEBUG: - call_command("runserver", f"0.0.0.0:{port}") + @staticmethod + def reset_huey_db(): + """Deletes the huey database""" + with contextlib.suppress(Exception): + if os.path.exists(HUEY_FILENAME): + os.remove(HUEY_FILENAME) + with contextlib.suppress(Exception): + if os.path.exists(f"{HUEY_FILENAME}-shm"): + os.remove(f"{HUEY_FILENAME}-shm") + with contextlib.suppress(Exception): + if os.path.exists(f"{HUEY_FILENAME}-wal"): + os.remove(f"{HUEY_FILENAME}-wal") def add_arguments(self, parser): parser.add_argument( @@ -115,20 +124,28 @@ def add_arguments(self, parser): help="Have Conreq set permissions during preconfig.", ) - @staticmethod - def reset_huey_db(): - """Deletes all entries within the Huey background task database.""" - with sqlite3.connect(HUEY_FILENAME) as cursor: - tables = list( - cursor.execute("select name from sqlite_master where type is 'table'") - ) - cursor.executescript(";".join(["delete from %s" % i for i in tables])) - @staticmethod - def start_huey(): - """Starts the Huey background task manager.""" - django.setup() - if DEBUG: - call_command("run_huey") - else: - call_command("run_huey", "--quiet") +def start_webserver(port): + django.setup() + + uvicorn.run( + "conreq.asgi:application", + host="0.0.0.0", + port=port, + ws_ping_interval=10, + workers=1 if DEBUG else (os.cpu_count() or 8), + access_log=ACCESS_LOG_FILE, + reload=DEBUG, + env_file=UVICORN_CONFIG if os.path.exists(UVICORN_CONFIG) else None, + ) + + +def start_huey(): + """Starts the Huey background task manager.""" + django.setup() + print("Starting Huey background task manager...") + + if DEBUG: + call_command("run_huey") + else: + call_command("run_huey", "--quiet") diff --git a/requirements/main.txt b/requirements/main.txt index f081d5de..5daffa40 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -9,7 +9,6 @@ django-silk==5.4.3 django-solo==2.4.0 django-url-or-relative-url-field==0.2.0 huey==2.5.4 -hypercorn[h3]==0.18.0 jsonfield==3.2.0 markdown==3.10.0 pwned-passwords-django==5.2.0 @@ -22,5 +21,6 @@ tmdbsimple==2.9.1 Twisted[tls,http2]==25.5.0 tzlocal==5.3.1 servestatic[brotli]==3.1.0 +uvicorn[standard]==0.22.0 attrs cffi From 992dc47c695aff53d1c88beaa1701ebbc3d321e8 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:05:41 -0800 Subject: [PATCH 08/13] better sqlite settings --- conreq/settings.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/conreq/settings.py b/conreq/settings.py index 380451af..aa7390c1 100644 --- a/conreq/settings.py +++ b/conreq/settings.py @@ -95,11 +95,20 @@ "name": "huey", # DB name for huey. "huey_class": "huey.SqliteHuey", # Huey implementation to use. "filename": HUEY_FILENAME, # Sqlite filename - "results": False, # Whether to return values of tasks. + "results": True, # Whether to return values of tasks. + "store_none": False, # Whether to store results of tasks that return None. "immediate": False, # If True, run tasks synchronously. "strict_fifo": True, # Utilize Sqlite AUTOINCREMENT to have unique task IDs + "timeout": 10, # Seconds to wait when reading from the DB. + "connection": { + "isolation_level": "IMMEDIATE", # Use immediate transactions to allow sqlite to respect `timeout`. + "cached_statements": 2000, # Number of pages to keep in memory. + }, "consumer": { - "workers": 20, + "workers": os.cpu_count() or 8, # Number of worker processes/threads. + "worker_type": "thread", # "thread" or "process" + "initial_delay": 0.25, # Smallest polling interval + "check_worker_health": True, # Whether to monitor worker health. }, } @@ -338,7 +347,17 @@ "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(DATA_DIR, "db.sqlite3"), "OPTIONS": { - "timeout": 3, # 3 second query timeout + "init_command": ( + "PRAGMA foreign_keys = ON;" + "PRAGMA journal_mode = WAL;" + "PRAGMA synchronous = NORMAL;" + "PRAGMA busy_timeout = 10000;" + "PRAGMA temp_store = MEMORY;" + "PRAGMA mmap_size = 134217728;" + "PRAGMA journal_size_limit = 67108864;" + "PRAGMA cache_size = 2000;" + ), + "transaction_mode": "IMMEDIATE", }, } } From 17e09e64cd4f85fd6c6be91fd6c961eafe4a9b91 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:39:04 -0800 Subject: [PATCH 09/13] fix docker --- Dockerfile | 14 ++++-- .../base/management/commands/run_conreq.py | 46 +++++++++++++------ requirements/main.txt | 2 +- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 71e2d42a..405934b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ -# FROM ghcr.io/linuxserver/baseimage-alpine:3.20 -FROM python:3.11.9-alpine3.20 +# BUILD COMMAND: +# docker build . --no-cache -t conreq +# mkdir config +# RUN COMMAND (Windows): +# docker run -p '7575:7575/tcp' -v $PWD/config:/config conreq +# RUN COMMAND (Linux/Mac): +# docker run -p '7575:7575/tcp' -v $(pwd)/config:/config conreq +FROM python:3.12.12-alpine3.21 ENV DATA_DIR=/config DEBUG=False @@ -41,7 +47,9 @@ RUN \ && \ echo "**** Install Python dependencies ****" \ && \ - pip3 install --no-cache-dir -U -r /app/conreq/requirements/main.txt \ + pip3 install uv \ + && \ + uv pip install --system --no-cache-dir -U -r /app/conreq/requirements/main.txt \ && \ echo "**** Cleanup ****" \ && \ diff --git a/conreq/core/base/management/commands/run_conreq.py b/conreq/core/base/management/commands/run_conreq.py index 4dd363e6..b098993b 100644 --- a/conreq/core/base/management/commands/run_conreq.py +++ b/conreq/core/base/management/commands/run_conreq.py @@ -1,5 +1,6 @@ import contextlib import os +import sys from logging import getLogger from multiprocessing import Process from time import sleep @@ -29,8 +30,6 @@ def handle(self, *args, **options): verbosity = "-v 1" if DEBUG else "-v 0" # Perform clean-up - print("Removing stale background tasks...") - self.reset_huey_db() if DEBUG: print("Clearing cache...") cache.clear() @@ -76,19 +75,6 @@ def handle(self, *args, **options): sleep(5) - @staticmethod - def reset_huey_db(): - """Deletes the huey database""" - with contextlib.suppress(Exception): - if os.path.exists(HUEY_FILENAME): - os.remove(HUEY_FILENAME) - with contextlib.suppress(Exception): - if os.path.exists(f"{HUEY_FILENAME}-shm"): - os.remove(f"{HUEY_FILENAME}-shm") - with contextlib.suppress(Exception): - if os.path.exists(f"{HUEY_FILENAME}-wal"): - os.remove(f"{HUEY_FILENAME}-wal") - def add_arguments(self, parser): parser.add_argument( "-p", @@ -149,3 +135,33 @@ def start_huey(): call_command("run_huey") else: call_command("run_huey", "--quiet") + + +# Patch if on a *nix system +if sys.platform != "win32": + # Monkey patch for https://github.com/Kludex/uvicorn/issues/2679 + from uvicorn import _subprocess + + def subprocess_started( + config, + target, + sockets, + stdin_fileno, + ) -> None: + """ + Called when the child process starts. + + * config - The Uvicorn configuration instance. + * target - A callable that accepts a list of sockets. In practice this will + be the `Server.run()` method. + * sockets - A list of sockets to pass to the server. Sockets are bound once + by the parent process, and then passed to the child processes. + * stdin_fileno - The file number of sys.stdin, so that it can be reattached + to the child process. + """ + config.configure_logging() + + with contextlib.suppress(KeyboardInterrupt): + target(sockets=sockets) + + _subprocess.subprocess_started = subprocess_started diff --git a/requirements/main.txt b/requirements/main.txt index 5daffa40..99122f59 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -21,6 +21,6 @@ tmdbsimple==2.9.1 Twisted[tls,http2]==25.5.0 tzlocal==5.3.1 servestatic[brotli]==3.1.0 -uvicorn[standard]==0.22.0 +uvicorn[standard]==0.38.0 attrs cffi From d9c5140755e1ef322d2c89610b6c3df265311166 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:53:47 -0800 Subject: [PATCH 10/13] fix visual bug where username would sometimes not be next to the profile icon --- conreq/core/base/static/css/sidebar.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conreq/core/base/static/css/sidebar.css b/conreq/core/base/static/css/sidebar.css index 50817ae8..53f98cd3 100644 --- a/conreq/core/base/static/css/sidebar.css +++ b/conreq/core/base/static/css/sidebar.css @@ -107,7 +107,7 @@ .sidebar-user .username { display: inline-flex; - width: calc(100% - 50px); + width: calc(100% - 55px); font-weight: 700; } From cbb4a27df307fa858fdb08ce396f17a14385204a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:54:00 -0800 Subject: [PATCH 11/13] remove deprecated compression middleware --- conreq/settings.py | 1 - requirements/main.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/conreq/settings.py b/conreq/settings.py index aa7390c1..c76df17d 100644 --- a/conreq/settings.py +++ b/conreq/settings.py @@ -278,7 +278,6 @@ *list_modules(APPS_DIR), # User Installed Apps ] MIDDLEWARE = [ - "compression_middleware.middleware.CompressionMiddleware", "django.middleware.security.SecurityMiddleware", "servestatic.middleware.ServeStaticMiddleware", # Serve static files through Django securely "django.middleware.gzip.GZipMiddleware", diff --git a/requirements/main.txt b/requirements/main.txt index 99122f59..1f82a398 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -1,7 +1,6 @@ channels[daphne]==4.3.2 django==5.2.8 django-cleanup==9.0.0 -django-compression-middleware==0.5.0 django-compressor==4.6 django-model-utils==5.0.0 django-searchable-encrypted-fields==0.2.1 From 91c8d573dc28219adc49cbe5ee6cde791bba046a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:30:29 -0800 Subject: [PATCH 12/13] Add quick request button to posters --- conreq/core/base/static/css/posters.css | 31 +++++++++-- .../core/base/static/css/simple_posters.css | 16 ++++-- conreq/core/base/static/js/events_click.js | 53 +++++++++++++++---- conreq/core/base/static/js/events_generic.js | 14 +++-- conreq/core/base/static/js/modal.js | 1 + conreq/core/base/static/js/viewport.js | 16 +++--- conreq/core/tmdb/templates/cards/poster.html | 7 ++- 7 files changed, 107 insertions(+), 31 deletions(-) diff --git a/conreq/core/base/static/css/posters.css b/conreq/core/base/static/css/posters.css index 05fb5254..b7b295a1 100644 --- a/conreq/core/base/static/css/posters.css +++ b/conreq/core/base/static/css/posters.css @@ -32,9 +32,6 @@ height: var(--poster-height); display: block; transition: transform 0.3s ease, opacity 2.5s, -webkit-filter 0.3s ease; - transition: filter 0.3s ease, transform 0.3s ease, opacity 2.5s; - transition: filter 0.3s ease, transform 0.3s ease, opacity 2.5s, - -webkit-filter 0.3s ease; z-index: 6; position: relative; } @@ -92,7 +89,8 @@ filter: brightness(25%); } -.viewport .poster-container .fa-angle-down { +.viewport .poster-container .fa-angle-down, +.viewport .poster-container .fa-plus { position: absolute; bottom: 0px; color: rgba(255, 255, 255, 0.5); @@ -106,11 +104,36 @@ text-align: center; } +.viewport .poster-container .fa-plus { + right: 5px; + top: calc(var(--poster-height) - 25px); + width: 20px; + height: 20px; + padding: unset; + color: #000000; + background: #e4e4e4; + opacity: 0.65; + align-content: center; + justify-content: center; + font-size: 12px; + line-height: 14px; + border-radius: 20px; + transition: color 0.3s ease, background 0.3s ease, opacity 0.3s ease; + -webkit-filter: drop-shadow(0 0 6px #000000); + filter: drop-shadow(0 0 6px #000000); +} + .viewport .poster-container .fa-angle-down:hover { background-color: rgb(255 255 255 / 10%); color: rgba(255, 255, 255, 0.85); } +.viewport .poster-container .fa-plus:hover { + color: var(--accent-color); + background: #ffffff; + opacity: 1; +} + .requested-by { position: absolute; top: calc(var(--poster-height) - 30px); diff --git a/conreq/core/base/static/css/simple_posters.css b/conreq/core/base/static/css/simple_posters.css index 89ce3599..479b0ae1 100644 --- a/conreq/core/base/static/css/simple_posters.css +++ b/conreq/core/base/static/css/simple_posters.css @@ -2,7 +2,8 @@ height: var(--poster-height); } -.viewport .poster-container .fa-angle-down { +.viewport .poster-container .fa-angle-down, +.viewport .poster-container .fa-plus { padding-top: 0px; color: #000000; background: #e4e4e4; @@ -19,11 +20,20 @@ transition: color 0.3s ease, background 0.3s ease, opacity 0.3s ease; } -.viewport .poster-container:hover .fa-angle-down { +.viewport .poster-container .fa-plus { + left: unset; + right: 5px; + bottom: 5px; + transform: none; +} + +.viewport .poster-container:hover .fa-angle-down, +.viewport .poster-container:hover .fa-plus { opacity: 1; } -.viewport .poster-container .fa-angle-down:hover { +.viewport .poster-container .fa-angle-down:hover, +.viewport .poster-container .fa-plus:hover { color: var(--accent-color); background: #ffffff; } diff --git a/conreq/core/base/static/js/events_click.js b/conreq/core/base/static/js/events_click.js index 9712376a..96c420a9 100644 --- a/conreq/core/base/static/js/events_click.js +++ b/conreq/core/base/static/js/events_click.js @@ -90,7 +90,7 @@ var quick_info_btn_click_event = async function () { var content_modal_click_event = async function () { $( - ".series-modal-btn, .content-preview-modal-btn, .report-selection-modal-btn" + ".series-modal-btn, .content-preview-modal-btn, .report-selection-modal-btn", ).each(async function () { let current_btn = $(this); if (!current_btn.hasClass("loaded")) { @@ -99,11 +99,11 @@ var content_modal_click_event = async function () { tmdb_id: $(this).data("tmdb-id"), content_type: $(this).data("content-type"), report_modal: $(this).hasClass( - "report-selection-modal-btn" + "report-selection-modal-btn", ), }; generate_modal( - $(this).data("modal-url") + "?" + $.param(params) + $(this).data("modal-url") + "?" + $.param(params), ); }); current_btn.addClass("loaded"); @@ -111,6 +111,39 @@ var content_modal_click_event = async function () { }); }; +var quick_request_click_event = async function () { + $(".quick-request-btn").each(async function () { + let btn = $(this); + btn.unbind("click"); + btn.click(async function () { + let params = { + tmdb_id: btn.data("tmdb-id"), + content_type: btn.data("content-type"), + seasons: null, + episode_ids: null, + }; + + // Prevent the user from spamming the request button + if (ongoing_request == btn) { + return false; + } else { + ongoing_request = btn; + } + + // Request the content + post_json(btn.data("request-url"), params, function () { + requested_toast_message(); + console.log(btn); + btn.remove(); + ongoing_request = null; + }).fail(async function () { + conreq_no_response_toast_message(); + ongoing_request = null; + }); + }); + }); +}; + var create_report_modal_click_event = async function () { $(".report-modal-btn").each(async function () { $(this).unbind("click"); @@ -124,7 +157,7 @@ var create_report_modal_click_event = async function () { if (params.content_type == "movie") { report_selection = null; generate_modal( - $(this).data("modal-url") + "?" + $.param(params) + $(this).data("modal-url") + "?" + $.param(params), ); } @@ -137,7 +170,7 @@ var create_report_modal_click_event = async function () { // All non-special seasons were checkboxed if (current_selection == true) { generate_modal( - $(this).data("modal-url") + "?" + $.param(params) + $(this).data("modal-url") + "?" + $.param(params), ); } @@ -149,7 +182,7 @@ var create_report_modal_click_event = async function () { ) { report_selection = current_selection; generate_modal( - $(this).data("modal-url") + "?" + $.param(params) + $(this).data("modal-url") + "?" + $.param(params), ); } @@ -239,7 +272,7 @@ var row_title_click_event = async function () { // Checkmark all related episodes let episode_container = $( - season_checkbox.data("all-suboptions-container") + season_checkbox.data("all-suboptions-container"), ); let episode_checkboxes = episode_container.find("input"); episode_checkboxes.prop("checked", season_checkbox.prop("checked")); @@ -251,7 +284,7 @@ var row_checkbox_click_event = async function () { // Checkmark all related episodes let season_checkbox = $(this); let episode_container = $( - season_checkbox.data("all-suboptions-container") + season_checkbox.data("all-suboptions-container"), ); let episode_checkboxes = episode_container.find("input"); episode_checkboxes.prop("checked", season_checkbox.prop("checked")); @@ -301,7 +334,7 @@ var row_suboption_checkbox_click_event = async function () { // Checkmark the season box if every episode is checked else { let all_episodes_container = $( - this.parentElement.parentElement.parentElement.parentElement + this.parentElement.parentElement.parentElement.parentElement, ); let episode_checkboxes = all_episodes_container.find("input"); let checkmarked_episode_checkboxes = @@ -455,7 +488,7 @@ var server_settings_dropdown_click_event = async function () { change_server_setting(setting_name, dropdown_id).then( async function () { generate_viewport(false); - } + }, ); }); }; diff --git a/conreq/core/base/static/js/events_generic.js b/conreq/core/base/static/js/events_generic.js index e0429966..68269be2 100644 --- a/conreq/core/base/static/js/events_generic.js +++ b/conreq/core/base/static/js/events_generic.js @@ -20,14 +20,14 @@ $(document).ready(async function () { // Server Settings $( - 'input[type="text"].settings-item.admin, input[type="url"].settings-item.admin' + 'input[type="text"].settings-item.admin, input[type="url"].settings-item.admin', ).each(async function () { let setting_name = $(this).data("setting-name"); let current_value = $(this).val(); previous_admin_settings[setting_name] = current_value; }); $( - 'input[type="text"].settings-item.admin, input[type="url"].settings-item.admin' + 'input[type="text"].settings-item.admin, input[type="url"].settings-item.admin', ).on("keypress", function (e) { let setting_name = $(this).data("setting-name"); let current_value = $(this).val(); @@ -37,7 +37,7 @@ $(document).ready(async function () { } }); $( - 'input[type="text"].settings-item.admin, input[type="url"].settings-item.admin' + 'input[type="text"].settings-item.admin, input[type="url"].settings-item.admin', ).focusout(async function () { let setting_name = $(this).data("setting-name"); let current_value = $(this).val(); @@ -69,6 +69,7 @@ $(document).ready(async function () { } content_modal_click_event(); + quick_request_click_event(); // Infinite Scroller if ($(".viewport-container>.infinite-scroll").length) { @@ -95,6 +96,7 @@ $(document).ready(async function () { masonry_grid.on("append.infiniteScroll", async function () { cull_old_posters(); content_modal_click_event(); + quick_request_click_event(); }); infinite_scroller_created = true; @@ -137,6 +139,7 @@ $(document).ready(async function () { // More Info page events request_btn_click_event(); content_modal_click_event(); + quick_request_click_event(); create_report_modal_click_event(); quick_info_btn_click_event(); more_info_poster_popup_click_event(); @@ -179,7 +182,7 @@ $(document).ready(async function () { ]); }); }); - } + }, ); $(".viewport-container, .viewport-container-top").on( @@ -189,12 +192,13 @@ $(document).ready(async function () { if (new_element.is(".carousel.auto-construct")) { viewport_carousel_constructor(); content_modal_click_event(); + quick_request_click_event(); } if (new_element.hasClass("auto-uncollapse")) { new_element.collapse("show"); } server_settings_dropdown_click_event(); - } + }, ); $(".sidebar").on("loaded", async function () { diff --git a/conreq/core/base/static/js/modal.js b/conreq/core/base/static/js/modal.js index dbfd1ec5..4d32c308 100644 --- a/conreq/core/base/static/js/modal.js +++ b/conreq/core/base/static/js/modal.js @@ -20,6 +20,7 @@ var generate_modal = async function (modal_url) { // Add click events request_btn_click_event(); content_modal_click_event(); + quick_request_click_event(); modal_select_all_btn_click_event(); modal_expand_btn_click_event(); row_title_click_event(); diff --git a/conreq/core/base/static/js/viewport.js b/conreq/core/base/static/js/viewport.js index a5a90ea2..805a9280 100644 --- a/conreq/core/base/static/js/viewport.js +++ b/conreq/core/base/static/js/viewport.js @@ -36,7 +36,7 @@ let update_active_tab = async function () { // Updates the page name let update_page_title = async function (viewport_selector) { let page_name = DOMPurify.sanitize( - $(viewport_selector + ">.page-name").val() + $(viewport_selector + ">.page-name").val(), ); let app_name = $("#app-name").val(); if (page_name) { @@ -68,7 +68,7 @@ let cached_viewport_exists = function () { let display_cached_viewport = async function () { let new_viewport = "main[data-url='" + get_window_location() + "']"; let old_viewport = $( - "main:not([data-url='" + get_window_location() + "'])" + "main:not([data-url='" + get_window_location() + "'])", ); $(new_viewport).trigger("prepare-cached"); update_active_tab(); @@ -97,7 +97,7 @@ let prepare_viewport = async function (viewport_selector) { let get_viewport = async function ( location, viewport_selector, - success = function () {} + success = function () {}, ) { $(viewport_selector).trigger("url-changed"); @@ -120,7 +120,7 @@ let get_viewport = async function ( await destroy_viewport(viewport_selector); $(viewport_selector + ">*").remove(); $(viewport_selector).append( - "

Could not connect to the server!

" + "

Could not connect to the server!

", ); $(viewport_selector + ">h1").css("text-align", "center"); select_active_viewport(viewport_selector); @@ -160,7 +160,7 @@ var generate_viewport = async function (standard_viewport_load = true) { window.history.replaceState( {}, null, - $(".nav-tab a").attr("href") + $(".nav-tab a").attr("href"), ); window_location = window.location.hash.split("#")[1]; } @@ -192,14 +192,14 @@ var generate_viewport = async function (standard_viewport_load = true) { // Display the new content $(viewport_selector + ">.loading-animation-container").hide(); $( - viewport_selector + ">*:not(.loading-animation-container)" + viewport_selector + ">*:not(.loading-animation-container)", ).show(); // Set scroll position if (standard_viewport_load) { $(viewport_selector).scrollTop(0); } - } + }, ); // If the page is taking too long to load, show a loading animation @@ -209,7 +209,7 @@ var generate_viewport = async function (standard_viewport_load = true) { select_active_viewport(viewport_selector); $(viewport_selector + ">.loading-animation-container").show(); $( - viewport_selector + ">*:not(.loading-animation-container)" + viewport_selector + ">*:not(.loading-animation-container)", ).hide(); } }, 500); diff --git a/conreq/core/tmdb/templates/cards/poster.html b/conreq/core/tmdb/templates/cards/poster.html index cad49a3c..afa02f9d 100644 --- a/conreq/core/tmdb/templates/cards/poster.html +++ b/conreq/core/tmdb/templates/cards/poster.html @@ -31,6 +31,11 @@ data-content-type="{{card.content_type}}" data-tmdb-id="{% firstof card.tmdbId card.id %}" data-modal-url="{% url 'more_info:content_preview_modal' %}"> + + +
@@ -45,4 +50,4 @@
{{card.overview}}
- \ No newline at end of file + From fd4f6a165ad6cf4ad8a689eae3789b5eba11aaba Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:40:31 -0800 Subject: [PATCH 13/13] fix #80 --- conreq/core/user_requests/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conreq/core/user_requests/tasks.py b/conreq/core/user_requests/tasks.py index d82c00fe..cbb81be8 100644 --- a/conreq/core/user_requests/tasks.py +++ b/conreq/core/user_requests/tasks.py @@ -7,7 +7,7 @@ _logger = log.get_logger(__name__) -@db_task(expires=120) +@db_task(retries=25, retry_delay=300) def sonarr_request_background_task(tvdb_id, request_params, sonarr_params): """Function that can be run in the background to request something on Sonarr""" try: @@ -55,7 +55,7 @@ def sonarr_request_background_task(tvdb_id, request_params, sonarr_params): ) -@db_task(expires=120) +@db_task(retries=25, retry_delay=300) def radarr_request_background_task(tmdb_id, radarr_params): """Function that can be run in the background to request something on Radarr""" try: