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 |
Campaigns
Other sites - YouTube | + YouTube
{% if messages %} @@ -264,7 +272,30 @@ and SunkwiBOT/twitch-drops-api.
Data fetched at :01, :16, :31, and :46. +
+ Prefer structured data? Use the API + or download datasets + instead of scraping. + diff --git a/templates/chzzk/campaign_detail.html b/templates/chzzk/campaign_detail.html index baaf1a6..e6fa5ac 100644 --- a/templates/chzzk/campaign_detail.html +++ b/templates/chzzk/campaign_detail.html @@ -15,7 +15,7 @@ href="{% url 'chzzk:campaign_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -113,11 +113,46 @@ {% endif %} - -
- {% if campaign.pc_link_url %}[details]{% endif %} - {% if campaign.account_link_url %}[connect]{% endif %} -
+
Campaign Info
diff --git a/templates/chzzk/dashboard.html b/templates/chzzk/dashboard.html index c7c6169..89ad3e1 100644 --- a/templates/chzzk/dashboard.html +++ b/templates/chzzk/dashboard.html @@ -13,21 +13,40 @@ href="{% url 'chzzk:campaign_feed_atom' %}" /> {% endblock extra_head %} {% block content %}

Active Chzzk Drops

CHZZK is South Korean alternative to Twitch.
- -
+ {% if active_campaigns %} {% for campaign in active_campaigns %} diff --git a/templates/core/about.html b/templates/core/about.html new file mode 100644 index 0000000..06a6f5b --- /dev/null +++ b/templates/core/about.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} +{% block title %} + About +{% endblock title %} +{% block content %} +

About ttvdrops.lovinator.space

+

+ ttvdrops.lovinator.space tracks limited-time free rewards (drops) across Twitch, Kick, and Chzzk. + Streaming platforms often give away free in-game items, channel perks, and other rewards + to viewers who meet certain conditions, like watching a streamer play a game for a set amount of time. +

+

+ These offers 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, archives their data for historical + reference, and lets you subscribe to RSS/Atom feeds so you never miss a new offer. +

+

How Drops Work

+

+ Twitch Drops are organized into Drops Campaigns: collections of + rewards distributed during a specified time period. Each campaign has a start and end date + and is tied to a specific game category. Within a campaign, you can have multiple + Drops, each with its own conditions (called Drop Rules, + also referred to as milestones or requirements) that viewers must meet to earn a reward. +

+

There are two types of drop requirements:

+ +

+ When a viewer meets a Drop's requirement, a Reward (also called a + Benefit) is granted: typically an in-game item or beta-access key. + To claim their rewards, viewers must first link their Twitch account with the game's + identity service via an Account Linking page, then visit their + Drops Inventory to claim. +

+

Campaigns pass through several states before and after they run:

+ +

+ A campaign can include multiple Drops that run simultaneously, for example, + one reward after 30 minutes of watching and another after 60 minutes. All time-based + Drops accrue watch time at the same time, so you can earn toward all of them at once. + Campaigns can run for up to 28 days, and only one category-wide campaign can run for + a given game at a time. +

+

Features

+ +

Similar projects

+

Other sites that track Twitch drops:

+ +

Contact

+

Found a bug, have a suggestion, or just want to say hi? Reach out any way you like:

+ +

Terms of Use

+

+ This site and its API are provided as-is, without warranty of any kind. + Data is collected from public sources and may contain errors or be out of date. + Please be respectful with API requests - limit to at most 1 request per second + and prefer dataset downloads 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). + This service may be rate-limited or blocked for abusive usage at the operator's discretion. +

+

License

+

+ The source code is licensed under the MIT License. All collected data is dedicated to the + public domain under CC0. +

+{% endblock content %} diff --git a/templates/core/dashboard.html b/templates/core/dashboard.html index 30847c9..9e723f8 100644 --- a/templates/core/dashboard.html +++ b/templates/core/dashboard.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load image_tags %} {% block title %} - Drops Dashboard + TTVDrops Dashboard {% endblock title %} {% block extra_head %} + {% endblock extra_head %} {% block content %} -

Open Drop Campaigns

- Some Drops campaigns may not be available in your region. + This dashboard shows currently active Drops Campaigns from Twitch, Kick, and Chzzk. + Campaigns are collections of rewards distributed during a specified time period. + Campaigns are grouped by game, with each card showing the campaign name, Drop Rules + (requirements such as watch time or subscription count), participating channels, and + available rewards. Some campaigns may not be available in your region. Learn more about Drops.


@@ -51,7 +55,7 @@ [atom] [discord] + title="Discord-flavored feed for Twitch campaigns">[discord-flavored] [api] [explain] @@ -114,23 +118,33 @@
{{ campaign_data.publisher }}
{% endif %} + {% if campaign_data.watch_range %} +
{{ campaign_data.watch_range }}
+ {% endif %} +
+ Started {{ campaign_data.campaign.start_at|timesince }} ago +
- {% if campaign_data.watch_range %}{{ campaign_data.watch_range }} ·{% endif %} - ends in {{ campaign_data.campaign.end_at|timeuntil }} + margin-top: 0" + title="{{ campaign_data.campaign.end_at|date:'M d, Y H:i T' }}"> + Ends in {{ campaign_data.campaign.end_at|timeuntil }}
{% if campaign_data.allowed_channels %} - {{ campaign_data.allowed_channels.0.display_name }}{% if campaign_data.allowed_channels|length > 1 %} +{{ campaign_data.allowed_channels|length|add:'-1' }} more{% endif %} + {% if campaign_data.allowed_channels %}{{ campaign_data.allowed_channels|length }} channel{{ campaign_data.allowed_channels|length|pluralize }}{% endif %} {% else %} any channel + text-decoration: none">Category-wide {% endif %}
@@ -179,61 +193,79 @@

Open Reward Campaigns

-

- Rewards are limited-time offers from top brands that you can unlock by - supporting your favorite Twitch creators. Visit your inventory - to access unlocked rewards. +

+ [rss] + [atom] + [discord-flavored] + [api] + [explain] +
+

+ Reward Campaigns are limited-time offers from brands that you can unlock by + supporting your favorite Twitch creators. To receive your rewards, you will + need to link your Twitch account with the brand's account linking page and + then claim the reward from your + Drops Inventory.

- + {% endif %}
[atom] [discord] + title="Discord-flavored feed for all Kick campaigns">[discord-flavored] + [api] [explain] @@ -256,8 +290,6 @@ {% for game_id, game_data in kick_campaigns_by_game.items %}
{{ org.name }} - {% if not forloop.last %}, {% endif %} + {% if not forloop.last %},{% endif %} {% endfor %} {% else %} {{ game_data.name }} @@ -313,8 +345,7 @@ flex-direction: column; flex-shrink: 0; width: 450px; - padding: 0.9rem; - border-radius: 8px"> + padding: 0.9rem"> {% endif %} + {% if min_watch %} +
+ {% if min_watch == max_watch %}{{ min_watch }}m watch{% else %}{{ min_watch }}m-{{ max_watch }}m watch{% endif %} +
+ {% endif %} +
+ Started {{ campaign_data.campaign.starts_at|timesince }} ago +
- {% if min_watch %}{% if min_watch == max_watch %}{{ min_watch }}m watch{% else %}{{ min_watch }}m-{{ max_watch }}m watch{% endif %} · {% endif %} - ends in {{ campaign_data.campaign.ends_at|timeuntil }} + margin-top: 0" + title="{{ campaign_data.campaign.ends_at|date:'M d, Y H:i T' }}"> + Ends in {{ campaign_data.campaign.ends_at|timeuntil }}
@@ -372,14 +415,12 @@ gap: 0.35rem; padding: 0.25rem 0.6rem; font-size: 1.1rem; - background: color-mix(in srgb, CanvasText 8%, transparent 92%); - border-radius: 6px"> + background: color-mix(in srgb, CanvasText 8%, transparent 92%)"> {% if reward.full_image_url %} {{ reward.name }} {% endif %} diff --git a/templates/core/debug.html b/templates/core/debug.html index c62e4b7..924c085 100644 --- a/templates/core/debug.html +++ b/templates/core/debug.html @@ -203,7 +203,7 @@ {% endif %}
-

Data Source Diff — Drop Campaigns

+

Data Source Diff - Drop Campaigns

Comparing campaigns by data_source field in the DB.

Only in SunkwiBOT/twitch-drops-api ({{ sunkibot_only_drops_count }})

{% if sunkibot_only_drops %} @@ -239,7 +239,7 @@ {% endif %}
-

Data Source Diff — Reward Campaigns

+

Data Source Diff - Reward Campaigns

Only in SunkwiBOT/twitch-drops-api ({{ sunkibot_only_rewards_count }})

{% if sunkibot_only_rewards %}
    diff --git a/templates/core/docs_rss.html b/templates/core/docs_rss.html index 49e41e2..b19c261 100644 --- a/templates/core/docs_rss.html +++ b/templates/core/docs_rss.html @@ -7,14 +7,14 @@

    RSS Feeds Documentation

    - You have three types of feeds available for Twitch drops data: RSS, Atom, and Discord. + You have three types of feeds available for Twitch drops data: RSS, Atom, and Discord-flavored. RSS and Atom feeds are similar and can be used in any RSS reader application. The main difference is that Atom feeds include additional metadata and support for more complex content, while RSS feeds are more widely supported by older applications.

    - Discord feeds are available under the /discord/ endpoints. These are Atom feeds + Discord-flavored feeds are available under the /discord/ endpoints. These are Atom feeds that include Discord relative timestamps (e.g., <t:1773450272:R>) for dates, - making them ideal for Discord bots and integrations. Future enhancements may include Discord-specific formatting or content. + making them ideal for use in Discord messages. They are standard Atom feeds formatted for Discord's Markdown syntax, not Discord bot integrations.

    Twitch JSON API documentation is available at @@ -33,7 +33,7 @@ Description RSS Atom - Discord + Discord-flavored @@ -162,7 +162,7 @@ Feed Type RSS Atom - Discord + Discord-flavored diff --git a/templates/kick/campaign_detail.html b/templates/kick/campaign_detail.html index db3b82d..fceb6d7 100644 --- a/templates/kick/campaign_detail.html +++ b/templates/kick/campaign_detail.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% block title %} - {{ campaign.name }} — Kick Campaign + {{ campaign.name }} - Kick Campaign {% endblock title %} {% block extra_head %} {% if campaign.category %} @@ -14,7 +14,7 @@ href="{% url 'kick:game_campaign_feed_atom' campaign.category.kick_id %}" /> {% endif %} {% endblock extra_head %} @@ -51,7 +51,8 @@ Campaigns > {{ campaign.name }} -

    + +
    {% if campaign.category %}
    Category
    @@ -148,27 +149,56 @@

    {% endif %} -
    - {% if campaign.category %} - [rss] - [atom] - [discord] - {% endif %} - [explain] - {% if campaign.connect_url %} - [connect] - {% endif %} - {% if campaign.url %}[about]{% endif %} - {% if campaign.channels.count == 0 %} - {% if campaign.category and campaign.rule_name == "Watch to redeem" %} - [watch] +
    +
    +
    +
    + External +
    +
    + {% if campaign.connect_url %} + [connect] + {% endif %} + {% if campaign.url %}[about]{% endif %} + {% if campaign.channels.count == 0 %} + {% if campaign.category and campaign.rule_name == "Watch to redeem" %} + [watch] + {% endif %} + {% endif %} +
    +
    + {% if campaign.category %} +
    +
    + Subscribe (?) +
    +
    Campaigns: + [rss] + [atom] + [discord-flavored] +
    +
    {% endif %} - {% endif %} -
    +
    +
    diff --git a/templates/kick/campaign_list.html b/templates/kick/campaign_list.html index 10a6bee..fb025c6 100644 --- a/templates/kick/campaign_list.html +++ b/templates/kick/campaign_list.html @@ -13,27 +13,45 @@ href="{% url 'kick:campaign_feed_atom' %}" /> {% endblock extra_head %} {% block content %}
    -

    List of Kick drops

    +

    List of

    Kick drops
    Kick > Campaigns
    Scraped from Kick API.
    - +
    {% endblock extra_head %} {% block content %} @@ -49,21 +49,13 @@

    {{ category.name }}

    - {% if category.kick_url %}

    {{ category.kick_url }}

    {% endif %} -
    + +
    ID
    {{ category.kick_id }} @@ -93,6 +85,33 @@ {{ expired_campaigns|length }}
    +

    diff --git a/templates/kick/category_list.html b/templates/kick/category_list.html index 5eadb02..7de65ea 100644 --- a/templates/kick/category_list.html +++ b/templates/kick/category_list.html @@ -13,7 +13,7 @@ href="{% url 'kick:game_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -22,14 +22,33 @@
    Kick > Games
    - + {% if categories %}
      {% for category in categories %} diff --git a/templates/kick/dashboard.html b/templates/kick/dashboard.html index 483652d..1029de5 100644 --- a/templates/kick/dashboard.html +++ b/templates/kick/dashboard.html @@ -13,7 +13,7 @@ href="{% url 'kick:campaign_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -22,16 +22,33 @@
      Scraped from Kick API.
      - - +
      {% if active_campaigns %} {% for campaign in active_campaigns %} diff --git a/templates/kick/org_list.html b/templates/kick/org_list.html index 5f0f1aa..29ba488 100644 --- a/templates/kick/org_list.html +++ b/templates/kick/org_list.html @@ -13,7 +13,7 @@ href="{% url 'kick:organization_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -22,15 +22,33 @@
      Kick > Organizations
      - + {% if orgs %}
        {% for org in orgs %} diff --git a/templates/kick/organization_detail.html b/templates/kick/organization_detail.html index d269703..c1fb89a 100644 --- a/templates/kick/organization_detail.html +++ b/templates/kick/organization_detail.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% block title %} - {{ org.name }} — Kick Organization + {{ org.name }} - Kick Organization {% endblock title %} {% block content %}
        @@ -31,7 +31,8 @@ {{ org.url }}

        {% endif %} -
        + +
        ID
        {{ org.kick_id }} diff --git a/templates/twitch/campaign_detail.html b/templates/twitch/campaign_detail.html index fb7e35b..7f367bc 100644 --- a/templates/twitch/campaign_detail.html +++ b/templates/twitch/campaign_detail.html @@ -16,7 +16,7 @@ href="{% url 'core:game_campaign_feed_atom' campaign.game.twitch_id %}" /> {% endif %} {% endblock extra_head %} @@ -47,7 +47,8 @@ {% endif %}

        {{ campaign.description|linebreaksbr }}

        -
        + +
        Published
        - -
        - - {% if campaign.details_url %}[details]{% endif %} - - {% if campaign.account_link_url %}[connect]{% endif %} - - {% if campaign.game %} - [rss] - [atom] - [discord] - [explain] - [api] - {% endif %} -
        + {% if campaign.game %} + + {% endif %}
        Campaign Info
        diff --git a/templates/twitch/campaign_list.html b/templates/twitch/campaign_list.html index b2ce75b..e6ebd37 100644 --- a/templates/twitch/campaign_list.html +++ b/templates/twitch/campaign_list.html @@ -15,7 +15,7 @@ href="{% url 'core:campaign_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -32,7 +32,7 @@ [atom] [discord] + title="Discord-flavored feed for all campaigns">[discord-flavored] [api] [explain] diff --git a/templates/twitch/dashboard.html b/templates/twitch/dashboard.html index f827953..d53bfd8 100644 --- a/templates/twitch/dashboard.html +++ b/templates/twitch/dashboard.html @@ -15,7 +15,7 @@ href="{% url 'core:campaign_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -35,7 +35,7 @@ [atom] [discord] + title="Discord-flavored feed for campaigns">[discord-flavored] [api] [explain] diff --git a/templates/twitch/game_detail.html b/templates/twitch/game_detail.html index a5b7e56..c9d4e07 100644 --- a/templates/twitch/game_detail.html +++ b/templates/twitch/game_detail.html @@ -15,7 +15,7 @@ href="{% url 'core:game_campaign_feed_atom' game.twitch_id %}" /> {% endif %} {% endblock extra_head %} {% block content %} -
        +
        -
        - {% if game.box_art_best_url %} - {% picture game.box_art_best_url alt="Box art for "|add:game.get_game_name width=160 %} - {% endif %} -
        - -
        -

        + {% if game.box_art_best_url %} + {% picture game.box_art_best_url alt="Box art for "|add:game.get_game_name width=160 %} + {% endif %} + +
        +

        {{ game.display_name }} {% if game.display_name != game.name and game.name %}({{ game.name }}){% endif %}

        - -
        - Twitch > Games > {{ game.display_name }} + + +
        + {{ total_campaigns }} campaign{{ total_campaigns|pluralize }} + {% if total_rewards %} · {{ total_rewards }} reward{{ total_rewards|pluralize }}{% endif %} + {% if first_campaign_date and last_campaign_date %} + · + + - + + {% endif %}
        -
        + +
        {% if owners %}
        Owned by
        @@ -60,6 +76,16 @@ {% endfor %}
        {% endif %} +
        Game ID
        +
        + {{ game.twitch_id }} +
        + {% if game.slug %} +
        Slug
        +
        + {{ game.slug }} +
        + {% endif %}
        Published
        ({{ game.updated_at|timesince }} ago)
        - -
        - Campaigns: - [rss] - [atom] - [discord] -
        -
        - Rewards: - [rss] - [atom] - [discord] -
        - + +
        -
        +

        {% if active_campaigns %}
        Active Campaigns
        diff --git a/templates/twitch/game_less_rewards.html b/templates/twitch/game_less_rewards.html index 347ca3c..82707e1 100644 --- a/templates/twitch/game_less_rewards.html +++ b/templates/twitch/game_less_rewards.html @@ -14,7 +14,7 @@ href="{% url 'core:game_less_reward_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -34,7 +34,7 @@ [atom][discord] + title="Discord-flavored feed for all game-less rewards">[discord-flavored] {% if active_campaigns %} @@ -65,7 +65,7 @@ {{ campaign.summary }} {% endif %} - + - + - +
        {{ campaign.brand|default:"—" }}{{ campaign.brand|default:"-" }} {% if campaign.is_sitewide %} Site-wide @@ -113,7 +113,7 @@ {{ campaign.summary }} {% endif %} {{ campaign.brand|default:"—" }}{{ campaign.brand|default:"-" }} {% if campaign.is_sitewide %} Site-wide @@ -161,7 +161,7 @@ {{ campaign.summary }} {% endif %} {{ campaign.brand|default:"—" }}{{ campaign.brand|default:"-" }} {% if campaign.is_sitewide %} Site-wide diff --git a/templates/twitch/games_grid.html b/templates/twitch/games_grid.html index fd60bd8..eaf53df 100644 --- a/templates/twitch/games_grid.html +++ b/templates/twitch/games_grid.html @@ -14,7 +14,7 @@ href="{% url 'core:game_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -31,7 +31,7 @@ [atom] [discord] + title="Discord-flavored feed for all games">[discord-flavored] [api] [explain] diff --git a/templates/twitch/games_list.html b/templates/twitch/games_list.html index add17fa..bca88ac 100644 --- a/templates/twitch/games_list.html +++ b/templates/twitch/games_list.html @@ -13,7 +13,7 @@ href="{% url 'core:game_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -29,7 +29,7 @@ [atom] [discord] + title="Discord-flavored feed for all games">[discord-flavored] [api] [explain] diff --git a/templates/twitch/org_list.html b/templates/twitch/org_list.html index 48176d5..d0d890f 100644 --- a/templates/twitch/org_list.html +++ b/templates/twitch/org_list.html @@ -14,7 +14,7 @@ [atom] [discord] + title="Discord-flavored feed for all organizations">[discord-flavored] [api] [explain] diff --git a/templates/twitch/organization_detail.html b/templates/twitch/organization_detail.html index 3f1a71a..03d4c94 100644 --- a/templates/twitch/organization_detail.html +++ b/templates/twitch/organization_detail.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load image_tags %} {% block title %} {{ organization.name }} {% endblock title %} @@ -15,38 +16,129 @@ href="{% url 'core:game_campaign_feed_atom' game.twitch_id %}" /> {% endfor %} {% endif %} {% endblock extra_head %} {% block content %} -

        {{ organization.name }}

        - -
        - Dashboard > Organizations > {{ organization.name }} -
        -
        -
        Published
        -
        - ({{ organization.added_at|timesince }} ago) -
        -
        Last updated
        -
        - ({{ organization.updated_at|timesince }} ago) -
        -
        -
        -

        Games by {{ organization.name }}

        +
        +

        {{ organization.name }}

        + +
        + {{ games_count }} game{{ games_count|pluralize }} + {% if total_campaigns %} · {{ total_campaigns }} campaign{{ total_campaigns|pluralize }}{% endif %} +
        + +
        +
        Organization ID
        +
        + {{ organization.twitch_id }} +
        +
        Published
        +
        + ({{ organization.added_at|timesince }} ago) +
        +
        Last updated
        +
        + ({{ organization.updated_at|timesince }} ago) +
        +
        + {% if campaign_stats %} +
        + {% if campaign_stats.active_campaigns %} +
        + Active + {{ campaign_stats.active_campaigns }} +
        + {% endif %} + {% if campaign_stats.upcoming_campaigns %} +
        + Upcoming + {{ campaign_stats.upcoming_campaigns }} +
        + {% endif %} + {% if campaign_stats.expired_campaigns %} +
        + Expired + {{ campaign_stats.expired_campaigns }} +
        + {% endif %} + {% if campaign_stats.first_campaign %} +
        + First campaign + +
        + {% endif %} + {% if campaign_stats.last_campaign %} +
        + Latest campaign + +
        + {% endif %} +
        + {% endif %} +
        +

        Games by {{ organization.name }}

        + + + + + + {% for game in games %} - + {% endfor %} diff --git a/templates/twitch/reward_campaign_detail.html b/templates/twitch/reward_campaign_detail.html index fcb2d6a..93c58fb 100644 --- a/templates/twitch/reward_campaign_detail.html +++ b/templates/twitch/reward_campaign_detail.html @@ -15,7 +15,7 @@ href="{% url 'core:reward_campaign_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -122,7 +122,7 @@ [atom][discord] + title="Discord-flavored feed for all reward campaigns">[discord-flavored][api][explain] diff --git a/templates/twitch/reward_campaign_list.html b/templates/twitch/reward_campaign_list.html index b6e25bf..f694728 100644 --- a/templates/twitch/reward_campaign_list.html +++ b/templates/twitch/reward_campaign_list.html @@ -14,7 +14,7 @@ href="{% url 'core:reward_campaign_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -39,7 +39,7 @@ [atom][discord] + title="Discord-flavored feed for all reward campaigns">[discord-flavored] {% if reward_campaigns %} diff --git a/templates/twitch/sitewide_rewards.html b/templates/twitch/sitewide_rewards.html index 3e6b2fa..8f2ed6d 100644 --- a/templates/twitch/sitewide_rewards.html +++ b/templates/twitch/sitewide_rewards.html @@ -14,7 +14,7 @@ href="{% url 'core:sitewide_reward_feed_atom' %}" /> {% endblock extra_head %} {% block content %} @@ -49,7 +49,7 @@ [atom][discord] + title="Discord-flavored feed for site-wide rewards">[discord-flavored][explain][no game] @@ -82,7 +82,7 @@ {{ campaign.summary }} {% endif %} - + - + - +
        GameCampaigns
        - {{ game }} + + + + {{ game.campaign_count|default:0 }}
        {{ campaign.brand|default:"—" }}{{ campaign.brand|default:"-" }} {{ campaign.brand|default:"—" }}{{ campaign.brand|default:"-" }} {{ campaign.brand|default:"—" }}{{ campaign.brand|default:"-" }}