diff --git a/README.md b/README.md index d6cc782..bdf280e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,64 @@ # ttvdrops -Get notified when a new drop is available on Twitch +Track limited-time free rewards across Twitch, Kick, and Chzzk. + +Streaming platforms often give away free in-game items, channel perks, and other rewards +(called "drops") to viewers who meet certain conditions, like watching a streamer play a +game for a set amount of time (Time-Based Drops) or subscribing to a participating channel +(Subscription-Based Drops). These offers are organized into "Drops Campaigns": +collections of rewards distributed during a specified time period, each with its own start +and end dates and requirements. Campaigns are only available for a limited window and can +be scattered across different platforms, making them easy to miss. + +ttvdrops.lovinator.space monitors these campaigns across multiple platforms, archives their data for +historical reference, and lets you subscribe to RSS/Atom feeds so you never miss a new +offer. All historical campaign data is available as [open datasets](https://github.com/TheLovinator1/ttvdrops) +licensed under CC0. + +## Features + +- **Multi-platform**: Twitch, Kick, and Chzzk +- **RSS/Atom feeds**: Subscribe to campaigns, games, organizations, and rewards +- **Historical archive**: Past campaigns remain searchable and downloadable +- **Search & browse**: Find campaigns by game, organization, channel, or reward +- **Open data**: All data is freely available under CC0 + +## Project links + +- **Website**: [ttvdrops.lovinator.space](https://ttvdrops.lovinator.space) +- **Repository**: [github.com/TheLovinator1/ttvdrops](https://github.com/TheLovinator1/ttvdrops) +- **Issue tracker**: [github.com/TheLovinator1/ttvdrops/issues](https://github.com/TheLovinator1/ttvdrops/issues) +- **Donate**: [github.com/sponsors/TheLovinator1](https://github.com/sponsors/TheLovinator1) +- **License**: MIT (code), CC0 (data) + +## Similar projects + +Other sites that track Twitch drops: + +- [twitchdrops.app](https://twitchdrops.app/) +- [Fenrisapps Twitch Drops](https://twitch-drops.fenrisapps.com/) +- [twitch-drops-api](https://github.com/SunkwiBOT/twitch-drops-api) (the API that powers some of these tools) +- [Drop Hunter](https://drophunter.app/) (paid, $3/mo) + +## Bug reports & feature requests + +Found a bug or have an idea? Please [open an issue](https://github.com/TheLovinator1/ttvdrops/issues/new). +Include as much detail as possible, including what you expected, what happened, and steps to reproduce. + +You can also email [tlovinator@gmail.com](mailto:tlovinator@gmail.com) or reach out on Discord +(**TheLovinator** / `TheLovinator#9276`). + +## Contributing + +Contributions are welcome! Here's how to get started: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/my-feature`) +3. Install dependencies with `uv sync` +4. Rename .env.example to .env (Probably want to use USE_SQLITE=True) +5. Make your changes +6. Run tests with `uv run pytest` +7. Submit a pull request ## TL;DR (Arch Linux + PostgreSQL) @@ -92,10 +150,10 @@ uv run pytest Celery powers all periodic and background work. Three services are required: -| Service file | Queues | Purpose | -|---|---|---| +| Service file | Queues | Purpose | +| -------------------------------- | ----------------------------------- | --------------------------------- | | `ttvdrops-celery-worker.service` | `imports`, `api-fetches`, `default` | Twitch/Kick/Chzzk imports, backup | -| `ttvdrops-celery-beat.service` | — | Periodic task scheduler (Beat) | +| `ttvdrops-celery-beat.service` | - | Periodic task scheduler (Beat) | Start workers manually during development: @@ -103,7 +161,7 @@ Start workers manually during development: # All-in-one worker (development) uv run celery -A config worker --queues imports,api-fetches,image-downloads,default --loglevel=info -# Beat scheduler (requires database Beat tables — run migrate first) +# Beat scheduler (requires database Beat tables, run migrate first) uv run celery -A config beat --scheduler django_celery_beat.schedulers:DatabaseScheduler --loglevel=info # Monitor tasks in the browser (optional) @@ -112,14 +170,14 @@ uv run celery -A config flower **Periodic tasks configured via `CELERY_BEAT_SCHEDULE`:** -| Task | Schedule | Queue | -|---|---|---| -| `twitch.tasks.scan_pending_twitch_files` | every 10 s | `imports` | -| `kick.tasks.import_kick_drops` | :01/:16/:31/:46 | `api-fetches` | -| `chzzk.tasks.discover_chzzk_campaigns` | every 2 h | `api-fetches` | -| `twitch.tasks.backup_database` | daily 02:15 UTC | `default` | -| `twitch.tasks.download_all_images` | Sunday 04:00 UTC | `image-downloads` | -| `twitch.tasks.import_chat_badges` | Sunday 03:00 UTC | `api-fetches` | +| Task | Schedule | Queue | +| ---------------------------------------- | ---------------- | ----------------- | +| `twitch.tasks.scan_pending_twitch_files` | every 10 s | `imports` | +| `kick.tasks.import_kick_drops` | :01/:16/:31/:46 | `api-fetches` | +| `chzzk.tasks.discover_chzzk_campaigns` | every 2 h | `api-fetches` | +| `twitch.tasks.backup_database` | daily 02:15 UTC | `default` | +| `twitch.tasks.download_all_images` | Sunday 04:00 UTC | `image-downloads` | +| `twitch.tasks.import_chat_badges` | Sunday 03:00 UTC | `api-fetches` | Image downloads also run **immediately** on record creation via `post_save` signals (`Game`, `DropCampaign`, `DropBenefit`, `RewardCampaign`). diff --git a/config/api.py b/config/api.py index 1d96550..c34ec47 100644 --- a/config/api.py +++ b/config/api.py @@ -8,8 +8,34 @@ from twitch.api import api as twitch_router api_v1 = NinjaAPI( title="TTVDrops API", version="1.0.0", + description=( + "Track limited-time free rewards across Twitch, Kick, and Chzzk. " + "All campaign, game, organization, and reward data is available " + "in structured JSON. No authentication required. " + "Please be nice and limit requests to at most 1 per second. " + "The datasets at /datasets/ are a better choice for bulk analysis. " + "If you need a higher rate limit for a legitimate project, please reach out " + "via tlovinator@gmail.com or on Discord (TheLovinator)." + ), urls_namespace="api-v1", docs_url="docs/", + servers=[ + {"url": "https://ttvdrops.lovinator.space", "description": "Production server"}, + ], + openapi_extra={ + "info": { + "contact": { + "name": "TheLovinator", + "email": "tlovinator@gmail.com", + "url": "https://github.com/TheLovinator1/ttvdrops", + }, + "license": { + "name": "CC0 1.0 Universal (data), MIT (code)", + "url": "https://github.com/TheLovinator1/ttvdrops/blob/main/LICENSE", + }, + "termsOfService": "https://ttvdrops.lovinator.space/about/", + }, + }, ) api_v1.add_router("/twitch/", twitch_router, url_name_prefix="twitch-api-v1") diff --git a/config/settings.py b/config/settings.py index b4f40ee..429d72b 100644 --- a/config/settings.py +++ b/config/settings.py @@ -158,6 +158,7 @@ MIDDLEWARE: list[str] = [ "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", + "core.middleware.TimezoneMiddleware", ] diff --git a/core/middleware.py b/core/middleware.py new file mode 100644 index 0000000..11aeaea --- /dev/null +++ b/core/middleware.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING +from zoneinfo import ZoneInfo + +from django.utils import timezone + +if TYPE_CHECKING: + from collections.abc import Callable + + from django.http import HttpRequest + from django.http import HttpResponse + + +class TimezoneMiddleware: + """Activate the user's timezone from a cookie set by the browser.""" + + def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: + """Store the get_response callable.""" + self.get_response: Callable[[HttpRequest], HttpResponse] = get_response + + def __call__(self, request: HttpRequest) -> HttpResponse: + """Activate the user's timezone if a valid cookie is present. + + Returns: + HttpResponse: The response from the next middleware or view. + """ + tzname: str | None = request.COOKIES.get("timezone") + if tzname: + try: + timezone.activate(ZoneInfo(tzname)) + except KeyError, ValueError, TypeError: + timezone.deactivate() + else: + timezone.deactivate() + + return self.get_response(request) diff --git a/core/urls.py b/core/urls.py index 9feaaf6..bdadae8 100644 --- a/core/urls.py +++ b/core/urls.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from django.urls import path +from core.views import about_view from core.views import dashboard from core.views import dataset_backup_download_view from core.views import dataset_backups_view @@ -71,6 +72,12 @@ urlpatterns: list[URLPattern | URLResolver] = [ view=dataset_backup_download_view, name="dataset_backup_download", ), + # /about/ + path( + route="about/", + view=about_view, + name="about", + ), # /docs/rss/ path( route="docs/rss/", diff --git a/core/views.py b/core/views.py index feac188..20c4c40 100644 --- a/core/views.py +++ b/core/views.py @@ -498,6 +498,31 @@ def sitemap_youtube_view(request: HttpRequest) -> HttpResponse: return HttpResponse(xml_content, content_type="application/xml") +# MARK: /about/ +def about_view(request: HttpRequest) -> HttpResponse: + """Static about page with project description and contact information. + + Args: + request: The HTTP request object. + + Returns: + HttpResponse: The rendered about page. + """ + seo_context: dict[str, Any] = _build_seo_context( + page_title="About ttvdrops.lovinator.space", + page_description=( + "Track limited-time free rewards across Twitch, Kick, Chzzk, and YouTube." + ), + robots_directive="index, follow", + ) + + return render( + request, + "core/about.html", + seo_context, + ) + + # MARK: /docs/rss/ def docs_rss_view(request: HttpRequest) -> HttpResponse: """View for /docs/rss that lists all available feeds and explains how to use them. diff --git a/pyproject.toml b/pyproject.toml index edf95f9..c5ae941 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ttvdrops" version = "0.1.0" -description = "Get notified when a new drop is available on Twitch." +description = "Track limited-time free rewards across Twitch, Kick, and Chzzk." readme = "README.md" requires-python = ">=3.14" dependencies = [ @@ -126,7 +126,7 @@ lint.ignore = [ ] "**/migrations/**" = ["RUF012"] "**/api.py" = [ - "PLR0913", # Allow many arguments for API endpoint functions + "PLR0913", # Allow many arguments for API endpoint functions ] [tool.djlint] diff --git a/templates/base.html b/templates/base.html index 12e5b7d..0492591 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,4 +1,10 @@ {% load static %} +
@@ -45,7 +51,7 @@ href="{% url 'core:campaign_feed_atom' %}" /> {% block extra_head %} {% endblock extra_head %} @@ -201,9 +207,11 @@ RSS | Debug | Dataset | + Report issue | Donate | GitHub | API Docs | + About |