diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..696f042 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +.env +.env.example +.git/ +.github/ +.pre-commit-config.yaml +.pytest_cache/ +.ruff_cache/ +.venv +.vscode/ +.vscode/ +*.json +*.log +*.py[codz] +**/__pycache__/ +*$py.class +archive/ +check_these_please/ +db.sqlite3 +db.sqlite3-journal +env.bak/ +env/ +ENV/ +responses/ +staticfiles/ +tests/ +venv.bak/ +venv/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 1188f98..f4d6caa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "appauthor", "appname", "ASGI", + "botuser", "collectstatic", "colorama", "createsuperuser", @@ -16,6 +17,9 @@ "dotenv", "dropcampaign", "elif", + "filterwarnings", + "granian", + "gunicorn", "Hellsén", "hostnames", "httpx", @@ -27,14 +31,17 @@ "Mailgun", "makemigrations", "McCabe", + "noinput", "noopener", "noreferrer", "platformdirs", "prefetcher", "psutil", + "pycache", "pydantic", "pydocstyle", "pygments", + "pyproject", "pyright", "pytest", "Ravendawn", @@ -42,12 +49,14 @@ "runserver", "sendgrid", "speculationrules", + "staticfiles", "testchannel", "testpass", "tqdm", "ttvdrops", "venv", "wrongpassword", + "wsgi", "xdist" ], "python.analysis.typeCheckingMode": "standard" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..18dfc41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:1 +FROM python:3.14-slim-trixie +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy +ENV UV_NO_DEV=1 +ENV UV_PYTHON_DOWNLOADS=0 + +WORKDIR /app +COPY . /app/ + +RUN uv sync +RUN chmod +x /app/start.sh + +ENV PATH="/app/.venv/bin:$PATH" + +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:8000/ || exit 1 + +VOLUME ["/home/root/.local/share/TTVDrops"] +EXPOSE 8000 +ENTRYPOINT [ "/app/start.sh" ] +CMD ["uv", "run", "gunicorn", "config.wsgi:application", "--workers", "27", "--bind", "0.0.0.0:8000"] diff --git a/config/settings.py b/config/settings.py index 409ad0f..dbb5ee4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -6,12 +6,10 @@ import sys from pathlib import Path from typing import Any -import django_stubs_ext from dotenv import load_dotenv from platformdirs import user_data_dir logger: logging.Logger = logging.getLogger("ttvdrops.settings") -django_stubs_ext.monkeypatch() load_dotenv(verbose=True) @@ -100,7 +98,7 @@ MEDIA_ROOT: Path = DATA_DIR / "media" MEDIA_ROOT.mkdir(exist_ok=True) MEDIA_URL = "/media/" -STATIC_ROOT: Path = BASE_DIR / "staticfiles" +STATIC_ROOT: Path = DATA_DIR / "staticfiles" STATIC_ROOT.mkdir(exist_ok=True) STATIC_URL = "static/" STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"] @@ -181,18 +179,3 @@ DATABASES: dict[str, dict[str, str | Path | dict[str, str]]] = { }, }, } - -TESTING: bool = "test" in sys.argv or "PYTEST_VERSION" in os.environ - -if not TESTING: - DEBUG_TOOLBAR_CONFIG: dict[str, str] = { - "ROOT_TAG_EXTRA_ATTRS": "hx-preserve", - } - INSTALLED_APPS = [ # pyright: ignore[reportConstantRedefinition] - *INSTALLED_APPS, - "debug_toolbar", - ] - MIDDLEWARE = [ # pyright: ignore[reportConstantRedefinition] - "debug_toolbar.middleware.DebugToolbarMiddleware", - *MIDDLEWARE, - ] diff --git a/config/urls.py b/config/urls.py index 84d6bf6..96da617 100644 --- a/config/urls.py +++ b/config/urls.py @@ -15,15 +15,6 @@ urlpatterns: list[URLResolver] | list[URLPattern | URLResolver] = [ # type: ign path(route="", view=include("twitch.urls", namespace="twitch")), ] -if not settings.TESTING: - # Import debug_toolbar lazily to avoid ImportError when not installed in testing environments - from debug_toolbar.toolbar import debug_toolbar_urls # pyright: ignore[reportMissingTypeStubs] - - urlpatterns = [ - *urlpatterns, - *debug_toolbar_urls(), - ] - # Serve media in development if settings.DEBUG: urlpatterns += static( diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..75b080a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + ttvdrops: + container_name: ttvdrops + build: . + ports: + - "8000:8000" + env_file: .env + environment: + - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY?Please set DJANGO_SECRET_KEY in your environment} + - DEBUG=1 + - EMAIL_HOST=${EMAIL_HOST:-smtp.example.com} + - EMAIL_PORT=${EMAIL_PORT:-587} + - EMAIL_HOST_USER=${EMAIL_HOST_USER:-} + - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-} + volumes: + # Data is stored in /home/root/.local/share/TTVDrops" inside the container + - ./data:/home/root/.local/share/TTVDrops + restart: unless-stopped diff --git a/pyproject.toml b/pyproject.toml index c9b6965..d19a25a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,27 +3,24 @@ name = "ttvdrops" version = "0.1.0" description = "Get notified when a new drop is available on Twitch." readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.14" dependencies = [ - "dateparser>=1.2.2", - "django-debug-toolbar>=5.2.0", - "django>=5.2.4", - "djlint>=1.36.4", - "json-repair>=0.50.0", - "platformdirs>=4.3.8", - "python-dotenv>=1.1.1", - "pygments>=2.19.2", - "httpx>=0.28.1", - "pydantic>=2.12.5", - "tqdm>=4.67.1", - "colorama>=0.4.6", - "django-stubs-ext>=5.2.8", - "django-stubs[compatible-mypy]>=5.2.8", - "types-pygments>=2.19.0.20251121", + "dateparser", + "django-debug-toolbar", + "django", + "json-repair", + "platformdirs", + "python-dotenv", + "pygments", + "httpx", + "pydantic", + "tqdm", + "colorama", + "gunicorn", ] [dependency-groups] -dev = ["pytest>=8.4.1", "pytest-django>=4.11.1"] +dev = ["pytest", "pytest-django", "djlint"] [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings" @@ -98,9 +95,3 @@ line-length = 120 [tool.djlint] profile = "django" ignore = "H021" - -[tool.mypy] -plugins = ["mypy_django_plugin.main"] - -[tool.django-stubs] -django_settings_module = "config.settings" diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..9361711 --- /dev/null +++ b/start.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +echo "Collecting static files..." +uv run python manage.py collectstatic --noinput + +echo "Starting Django server..." +exec "$@" diff --git a/static/about.txt b/static/about.txt new file mode 100644 index 0000000..6e2355d --- /dev/null +++ b/static/about.txt @@ -0,0 +1,6 @@ +This favicon was generated using the following graphics from Twemoji: + +- Graphics Title: 1f4a6.svg +- Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/jdecked/twemoji) +- Graphics Source: https://github.com/jdecked/twemoji/blob/master/assets/svg/1f4a6.svg +- Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/) diff --git a/static/android-chrome-192x192.png b/static/android-chrome-192x192.png new file mode 100644 index 0000000..0a655af Binary files /dev/null and b/static/android-chrome-192x192.png differ diff --git a/static/android-chrome-512x512.png b/static/android-chrome-512x512.png new file mode 100644 index 0000000..393a06e Binary files /dev/null and b/static/android-chrome-512x512.png differ diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000..6085fb7 Binary files /dev/null and b/static/apple-touch-icon.png differ diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png new file mode 100644 index 0000000..c94fe64 Binary files /dev/null and b/static/favicon-16x16.png differ diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png new file mode 100644 index 0000000..35817ce Binary files /dev/null and b/static/favicon-32x32.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..ffe57c6 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/site.webmanifest b/static/site.webmanifest new file mode 100644 index 0000000..6c094f8 --- /dev/null +++ b/static/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "ttvdrops", + "short_name": "ttvdrops", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/templates/base.html b/templates/base.html index 6f7b641..09f2956 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,74 +1,156 @@ +{% load static %} - - - - + + + + + + - + content="Twitch Drops Tracker - Track your Twitch drops and campaigns easily." /> + {% block title %} ttvdrops {% endblock title %} @@ -86,7 +168,7 @@ + value="{{ request.GET.q }}" /> {% if messages %} diff --git a/twitch/management/commands/better_import_drops.py b/twitch/management/commands/better_import_drops.py index 48ce75d..f6ebede 100644 --- a/twitch/management/commands/better_import_drops.py +++ b/twitch/management/commands/better_import_drops.py @@ -322,7 +322,7 @@ class Command(BaseCommand): for name, model, cache_attr in progress_bar: self.load_cache_for_model(progress_bar, name, model, cache_attr) tqdm.write("") - except (DatabaseError, OSError, RuntimeError, ValueError, TypeError): + except DatabaseError, OSError, RuntimeError, ValueError, TypeError: # If cache loading fails completely, just use empty caches tqdm.write(f"{Fore.YELLOW}⚠{Style.RESET_ALL} Cache preload skipped (database error)\n") @@ -1120,7 +1120,7 @@ class Command(BaseCommand): campaign_structure=campaign_structure, ) - except (ValidationError, json.JSONDecodeError): + except ValidationError, json.JSONDecodeError: if options["crash_on_error"]: raise @@ -1229,7 +1229,7 @@ class Command(BaseCommand): progress_bar.update(1) progress_bar.write(f"{Fore.GREEN}✓{Style.RESET_ALL} {file_path.name}") - except (ValidationError, json.JSONDecodeError): + except ValidationError, json.JSONDecodeError: if options["crash_on_error"]: raise