Add support for Reward campaigns
This commit is contained in:
.gitignore.pre-commit-config.yaml
.vscode
core
manage.pypoetry.lockpyproject.tomltwitch_app
@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@ -10,17 +11,12 @@ from platformdirs import user_data_dir
|
||||
from playwright.async_api import Playwright, async_playwright
|
||||
from playwright.async_api._generated import Response
|
||||
|
||||
from twitch_app.models import (
|
||||
Drop,
|
||||
DropCampaign,
|
||||
Game,
|
||||
Organization,
|
||||
)
|
||||
from twitch_app.models import Game, Image, Reward, RewardCampaign, UnlockRequirements
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from playwright.async_api._generated import BrowserContext, Page
|
||||
|
||||
# Where to store the Firefox profile
|
||||
# Where to store the Chrome profile
|
||||
data_dir = Path(
|
||||
user_data_dir(
|
||||
appname="TTVDrops",
|
||||
@ -37,120 +33,92 @@ if not data_dir:
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def insert_data(data: dict) -> None: # noqa: C901, PLR0914
|
||||
"""Insert data into the database.
|
||||
|
||||
Args:
|
||||
data: The data from Twitch.
|
||||
"""
|
||||
user_data: dict = data.get("data", {}).get("user")
|
||||
if not user_data:
|
||||
logger.debug("No user data found")
|
||||
return
|
||||
|
||||
user_data["id"]
|
||||
drop_campaign_data = user_data["dropCampaign"]
|
||||
if not drop_campaign_data:
|
||||
logger.debug("No drop campaign data found")
|
||||
return
|
||||
|
||||
# Create or get the organization
|
||||
owner_data = drop_campaign_data["owner"]
|
||||
owner, created = await sync_to_async(Organization.objects.get_or_create)(
|
||||
id=owner_data["id"],
|
||||
defaults={"name": owner_data["name"]},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Organization created: %s", owner)
|
||||
else:
|
||||
logger.debug("Organization found: %s", owner)
|
||||
|
||||
# Create or get the game
|
||||
game_data = drop_campaign_data["game"]
|
||||
game, created = await sync_to_async(Game.objects.get_or_create)(
|
||||
id=game_data["id"],
|
||||
defaults={
|
||||
"slug": game_data["slug"],
|
||||
"display_name": game_data["displayName"],
|
||||
"organization": owner,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Game created: %s", game)
|
||||
|
||||
# Create the drop campaign
|
||||
drop_campaign, created = await sync_to_async(DropCampaign.objects.get_or_create)(
|
||||
id=drop_campaign_data["id"],
|
||||
defaults={
|
||||
"account_link_url": drop_campaign_data.get("accountLinkURL"),
|
||||
"description": drop_campaign_data.get("description"),
|
||||
"details_url": drop_campaign_data.get("detailsURL"),
|
||||
"end_at": drop_campaign_data.get("endAt"),
|
||||
"image_url": drop_campaign_data.get("imageURL"),
|
||||
"name": drop_campaign_data.get("name"),
|
||||
"start_at": drop_campaign_data.get("startAt"),
|
||||
"status": drop_campaign_data.get("status"),
|
||||
"game": game,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Drop campaign created: %s", drop_campaign)
|
||||
|
||||
# Create time-based drops
|
||||
for drop_data in drop_campaign_data["timeBasedDrops"]:
|
||||
drop_benefit_edges = drop_data["benefitEdges"]
|
||||
|
||||
time_based_drop, created = await sync_to_async(Drop.objects.get_or_create)(
|
||||
id=drop_data["id"],
|
||||
defaults={
|
||||
"required_subs": drop_data.get("requiredSubs"),
|
||||
"end_at": drop_data.get("endAt"),
|
||||
"name": drop_data.get("name"),
|
||||
"required_minutes_watched": drop_data.get("requiredMinutesWatched"),
|
||||
"start_at": drop_data.get("startAt"),
|
||||
"drop_campaign": drop_campaign,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Time-based drop created: %s", time_based_drop)
|
||||
|
||||
for edge in drop_benefit_edges:
|
||||
benefit_data = edge["benefit"]
|
||||
benefit_owner_data = benefit_data["ownerOrganization"]
|
||||
|
||||
org, created = await sync_to_async(
|
||||
Organization.objects.get_or_create,
|
||||
)(
|
||||
id=benefit_owner_data["id"],
|
||||
defaults={"name": benefit_owner_data["name"]},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Organization created: %s", org)
|
||||
|
||||
benefit_game_data = benefit_data["game"]
|
||||
benefit_game, created = await sync_to_async(Game.objects.get_or_create)(
|
||||
id=benefit_game_data["id"],
|
||||
defaults={"display_name": benefit_game_data["name"]},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Benefit game created: %s", benefit_game)
|
||||
|
||||
# Get the drop to add the data to
|
||||
drop, created = await sync_to_async(Drop.objects.get_or_create)(
|
||||
id=drop_data["id"],
|
||||
async def add_reward_campaign(json_data: dict) -> None:
|
||||
"""Add data from JSON to the database."""
|
||||
for campaign_data in json_data["data"]["rewardCampaignsAvailableToUser"]:
|
||||
# Add or get Game
|
||||
game_data = campaign_data["game"]
|
||||
if game_data:
|
||||
game, _ = await sync_to_async(Game.objects.get_or_create)(
|
||||
id=game_data["id"],
|
||||
slug=game_data["slug"],
|
||||
defaults={
|
||||
"created_at": benefit_data.get("createdAt"),
|
||||
"entitlement_limit": benefit_data.get("entitlementLimit"),
|
||||
"image_asset_url": benefit_data.get("imageAssetURL"),
|
||||
"is_ios_available": benefit_data.get("isIosAvailable"),
|
||||
"name": benefit_data.get("name"),
|
||||
"display_name": game_data["displayName"],
|
||||
"typename": game_data["__typename"],
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.warning("%s is not for a game?", campaign_data["name"])
|
||||
game = None
|
||||
|
||||
if created:
|
||||
logger.debug("Drop created: %s", drop)
|
||||
# Add or get Image
|
||||
image_data = campaign_data["image"]
|
||||
image, _ = await sync_to_async(Image.objects.get_or_create)(
|
||||
image1_x_url=image_data["image1xURL"],
|
||||
defaults={"typename": image_data["__typename"]},
|
||||
)
|
||||
|
||||
await sync_to_async(drop.save)()
|
||||
# Create Reward instances
|
||||
rewards = []
|
||||
for reward_data in campaign_data["rewards"]:
|
||||
banner_image_data = reward_data["bannerImage"]
|
||||
banner_image, _ = await sync_to_async(Image.objects.get_or_create)(
|
||||
image1_x_url=banner_image_data["image1xURL"],
|
||||
defaults={"typename": banner_image_data["__typename"]},
|
||||
)
|
||||
|
||||
thumbnail_image_data = reward_data["thumbnailImage"]
|
||||
thumbnail_image, _ = await sync_to_async(Image.objects.get_or_create)(
|
||||
image1_x_url=thumbnail_image_data["image1xURL"],
|
||||
defaults={"typename": thumbnail_image_data["__typename"]},
|
||||
)
|
||||
|
||||
reward, _ = await sync_to_async(Reward.objects.get_or_create)(
|
||||
id=reward_data["id"],
|
||||
name=reward_data["name"],
|
||||
banner_image=banner_image,
|
||||
thumbnail_image=thumbnail_image,
|
||||
earnable_until=datetime.fromisoformat(reward_data["earnableUntil"].replace("Z", "+00:00")),
|
||||
redemption_instructions=reward_data["redemptionInstructions"],
|
||||
redemption_url=reward_data["redemptionURL"],
|
||||
typename=reward_data["__typename"],
|
||||
)
|
||||
rewards.append(reward)
|
||||
|
||||
# Add or get Unlock Requirements
|
||||
unlock_requirements_data = campaign_data["unlockRequirements"]
|
||||
_, _ = await sync_to_async(UnlockRequirements.objects.get_or_create)(
|
||||
subs_goal=unlock_requirements_data["subsGoal"],
|
||||
defaults={
|
||||
"minute_watched_goal": unlock_requirements_data["minuteWatchedGoal"],
|
||||
"typename": unlock_requirements_data["__typename"],
|
||||
},
|
||||
)
|
||||
|
||||
# Create Reward Campaign
|
||||
reward_campaign, _ = await sync_to_async(RewardCampaign.objects.get_or_create)(
|
||||
id=campaign_data["id"],
|
||||
name=campaign_data["name"],
|
||||
brand=campaign_data["brand"],
|
||||
starts_at=datetime.fromisoformat(campaign_data["startsAt"].replace("Z", "+00:00")),
|
||||
ends_at=datetime.fromisoformat(campaign_data["endsAt"].replace("Z", "+00:00")),
|
||||
status=campaign_data["status"],
|
||||
summary=campaign_data["summary"],
|
||||
instructions=campaign_data["instructions"],
|
||||
external_url=campaign_data["externalURL"],
|
||||
reward_value_url_param=campaign_data["rewardValueURLParam"],
|
||||
about_url=campaign_data["aboutURL"],
|
||||
is_sitewide=campaign_data["isSitewide"],
|
||||
game=game,
|
||||
image=image,
|
||||
typename=campaign_data["__typename"],
|
||||
)
|
||||
|
||||
# Add Rewards to the Campaign
|
||||
for reward in rewards:
|
||||
await sync_to_async(reward_campaign.rewards.add)(reward)
|
||||
|
||||
await sync_to_async(reward_campaign.save)()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -217,7 +185,7 @@ class Command(BaseCommand):
|
||||
logger.debug("Page loaded. Scraping data...")
|
||||
|
||||
# Wait 5 seconds for the page to load
|
||||
await asyncio.sleep(5)
|
||||
# await asyncio.sleep(5)
|
||||
|
||||
await browser.close()
|
||||
|
||||
@ -226,14 +194,16 @@ class Command(BaseCommand):
|
||||
if not isinstance(campaign, dict):
|
||||
continue
|
||||
|
||||
if "dropCampaign" in campaign.get("data", {}).get("user", {}):
|
||||
if "rewardCampaignsAvailableToUser" in campaign["data"]:
|
||||
await add_reward_campaign(campaign)
|
||||
|
||||
if "dropCampaign" in campaign.get("data", {}).get("user", {}): # noqa: SIM102
|
||||
if not campaign["data"]["user"]["dropCampaign"]:
|
||||
continue
|
||||
|
||||
await insert_data(campaign)
|
||||
|
||||
if "dropCampaigns" in campaign.get("data", {}).get("user", {}):
|
||||
await insert_data(campaign)
|
||||
msg = "Multiple dropCampaigns not supported"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
return json_data
|
||||
|
||||
|
@ -1,160 +1,90 @@
|
||||
# Generated by Django 5.1b1 on 2024-07-09 22:26
|
||||
# Generated by Django 5.1rc1 on 2024-07-30 21:49
|
||||
|
||||
import uuid
|
||||
|
||||
import auto_prefetch
|
||||
import django.db.models.deletion
|
||||
import django.db.models.functions.text
|
||||
import django.db.models.manager
|
||||
from django.db import migrations, models
|
||||
from django.db.migrations.operations.base import Operation
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
dependencies: list[tuple[str, str]] = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DropCampaign",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("account_link_url", models.URLField(blank=True, null=True)),
|
||||
("description", models.TextField(blank=True, null=True)),
|
||||
("details_url", models.URLField(blank=True, null=True)),
|
||||
("end_at", models.DateTimeField(blank=True, null=True)),
|
||||
("image_url", models.URLField(blank=True, null=True)),
|
||||
("name", models.TextField(blank=True, null=True)),
|
||||
("start_at", models.DateTimeField(blank=True, null=True)),
|
||||
("status", models.TextField(blank=True, null=True)),
|
||||
("added_at", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Drop Campaign",
|
||||
"verbose_name_plural": "Drop Campaigns",
|
||||
"ordering": ("name",),
|
||||
"abstract": False,
|
||||
"base_manager_name": "prefetch_manager",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.db.models.manager.Manager()),
|
||||
("prefetch_manager", django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
operations: list[Operation] = [
|
||||
migrations.CreateModel(
|
||||
name="Game",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("slug", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"twitch_url",
|
||||
models.GeneratedField( # type: ignore # noqa: PGH003
|
||||
db_persist=True,
|
||||
expression=django.db.models.functions.text.Concat(
|
||||
models.Value("https://www.twitch.tv/directory/category/"),
|
||||
"slug",
|
||||
),
|
||||
output_field=models.TextField(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"image_url",
|
||||
models.GeneratedField( # type: ignore # noqa: PGH003
|
||||
db_persist=True,
|
||||
expression=django.db.models.functions.text.Concat(
|
||||
models.Value("https://static-cdn.jtvnw.net/ttv-boxart/"),
|
||||
"id",
|
||||
models.Value("_IGDB.jpg"),
|
||||
),
|
||||
output_field=models.URLField(),
|
||||
),
|
||||
),
|
||||
("display_name", models.TextField(blank=True, null=True)),
|
||||
("added_at", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Game",
|
||||
"verbose_name_plural": "Games",
|
||||
"ordering": ("display_name",),
|
||||
"abstract": False,
|
||||
"base_manager_name": "prefetch_manager",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.db.models.manager.Manager()),
|
||||
("prefetch_manager", django.db.models.manager.Manager()),
|
||||
("id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("slug", models.TextField()),
|
||||
("display_name", models.TextField()),
|
||||
("typename", models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Organization",
|
||||
name="Image",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("name", models.TextField(blank=True, null=True)),
|
||||
("added_at", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Organization",
|
||||
"verbose_name_plural": "Organizations",
|
||||
"ordering": ("name",),
|
||||
"abstract": False,
|
||||
"base_manager_name": "prefetch_manager",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.db.models.manager.Manager()),
|
||||
("prefetch_manager", django.db.models.manager.Manager()),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("image1_x_url", models.URLField()),
|
||||
("typename", models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Drop",
|
||||
name="UnlockRequirements",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("created_at", models.DateTimeField(blank=True, null=True)),
|
||||
("entitlement_limit", models.IntegerField(blank=True, null=True)),
|
||||
("image_asset_url", models.URLField(blank=True, null=True)),
|
||||
("name", models.TextField(blank=True, null=True)),
|
||||
("added_at", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True, null=True)),
|
||||
("required_subs", models.IntegerField(blank=True, null=True)),
|
||||
("end_at", models.DateTimeField(blank=True, null=True)),
|
||||
("required_minutes_watched", models.IntegerField(blank=True, null=True)),
|
||||
("start_at", models.DateTimeField(blank=True, null=True)),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("subs_goal", models.IntegerField()),
|
||||
("minute_watched_goal", models.IntegerField()),
|
||||
("typename", models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Reward",
|
||||
fields=[
|
||||
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
("name", models.TextField()),
|
||||
("earnable_until", models.DateTimeField()),
|
||||
("redemption_instructions", models.TextField()),
|
||||
("redemption_url", models.URLField()),
|
||||
("typename", models.TextField()),
|
||||
(
|
||||
"drop_campaign",
|
||||
auto_prefetch.ForeignKey(
|
||||
"banner_image",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="drops",
|
||||
to="twitch_app.dropcampaign",
|
||||
related_name="banner_rewards",
|
||||
to="twitch_app.image",
|
||||
),
|
||||
),
|
||||
(
|
||||
"thumbnail_image",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="thumbnail_rewards",
|
||||
to="twitch_app.image",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Drop",
|
||||
"verbose_name_plural": "Drops",
|
||||
"ordering": ("name",),
|
||||
"abstract": False,
|
||||
"base_manager_name": "prefetch_manager",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.db.models.manager.Manager()),
|
||||
("prefetch_manager", django.db.models.manager.Manager()),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RewardCampaign",
|
||||
fields=[
|
||||
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
("name", models.TextField()),
|
||||
("brand", models.TextField()),
|
||||
("starts_at", models.DateTimeField()),
|
||||
("ends_at", models.DateTimeField()),
|
||||
("status", models.TextField()),
|
||||
("summary", models.TextField()),
|
||||
("instructions", models.TextField()),
|
||||
("external_url", models.URLField()),
|
||||
("reward_value_url_param", models.TextField()),
|
||||
("about_url", models.URLField()),
|
||||
("is_sitewide", models.BooleanField()),
|
||||
("typename", models.TextField()),
|
||||
("game", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="twitch_app.game")),
|
||||
("image", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="twitch_app.image")),
|
||||
("rewards", models.ManyToManyField(to="twitch_app.reward")),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="dropcampaign",
|
||||
name="game",
|
||||
field=auto_prefetch.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="drop_campaigns",
|
||||
to="twitch_app.game",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="organization",
|
||||
field=auto_prefetch.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="games",
|
||||
to="twitch_app.organization",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -0,0 +1,84 @@
|
||||
# Generated by Django 5.1rc1 on 2024-07-30 23:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('twitch_app', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='about_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='brand',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='ends_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='external_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='game',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reward_campaigns', to='twitch_app.game'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='image',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reward_campaigns', to='twitch_app.image'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='instructions',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='is_sitewide',
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='name',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='reward_value_url_param',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='starts_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='status',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='summary',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,109 @@
|
||||
# Generated by Django 5.1rc1 on 2024-07-30 23:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('twitch_app', '0002_alter_rewardcampaign_about_url_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='game',
|
||||
name='display_name',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='game',
|
||||
name='slug',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='game',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='image1_x_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='banner_image',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='banner_rewards', to='twitch_app.image'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='earnable_until',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='name',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='redemption_instructions',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='redemption_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='thumbnail_image',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='thumbnail_rewards', to='twitch_app.image'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='ends_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='is_sitewide',
|
||||
field=models.BooleanField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='rewards',
|
||||
field=models.ManyToManyField(related_name='reward_campaigns', to='twitch_app.reward'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='starts_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unlockrequirements',
|
||||
name='minute_watched_goal',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unlockrequirements',
|
||||
name='subs_goal',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unlockrequirements',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1rc1 on 2024-07-30 23:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('twitch_app', '0003_alter_game_display_name_alter_game_slug_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='id',
|
||||
field=models.UUIDField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='id',
|
||||
field=models.UUIDField(primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
@ -1,123 +1,209 @@
|
||||
from typing import Literal
|
||||
|
||||
import auto_prefetch
|
||||
from django.db import models
|
||||
from django.db.models import Value
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
|
||||
class Organization(auto_prefetch.Model):
|
||||
"""The company that owns the game.
|
||||
class Game(models.Model):
|
||||
"""The game that the reward is for.
|
||||
|
||||
For example, 2K games.
|
||||
Attributes:
|
||||
id (int): The primary key of the game.
|
||||
slug (str): The slug identifier of the game.
|
||||
display_name (str): The display name of the game.
|
||||
typename (str): The type name of the object, typically "Game".
|
||||
|
||||
JSON example:
|
||||
{
|
||||
"id": "780302568",
|
||||
"slug": "xdefiant",
|
||||
"displayName": "XDefiant",
|
||||
"__typename": "Game"
|
||||
}
|
||||
"""
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
name = models.TextField(blank=True, null=True)
|
||||
added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
|
||||
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
verbose_name: str = "Organization"
|
||||
verbose_name_plural: str = "Organizations"
|
||||
ordering: tuple[Literal["name"]] = ("name",)
|
||||
id = models.AutoField(primary_key=True)
|
||||
slug = models.TextField(null=True, blank=True)
|
||||
display_name = models.TextField(null=True, blank=True)
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name or self.id
|
||||
return self.display_name or "Unknown"
|
||||
|
||||
def get_twitch_url(self) -> str:
|
||||
return f"https://www.twitch.tv/directory/game/{self.slug}"
|
||||
|
||||
|
||||
class Game(auto_prefetch.Model):
|
||||
"""The game that the drop campaign is for.
|
||||
class Image(models.Model):
|
||||
"""An image model representing URLs and type.
|
||||
|
||||
For example, MultiVersus.
|
||||
Attributes:
|
||||
image1_x_url (str): URL to the image.
|
||||
typename (str): The type name of the object, typically "RewardCampaignImageSet".
|
||||
|
||||
JSON example:
|
||||
{
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
}
|
||||
"""
|
||||
|
||||
organization = auto_prefetch.ForeignKey(
|
||||
Organization,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="games",
|
||||
)
|
||||
id = models.TextField(primary_key=True)
|
||||
slug = models.TextField(blank=True, null=True)
|
||||
twitch_url = models.GeneratedField( # type: ignore # noqa: PGH003
|
||||
expression=Concat(Value("https://www.twitch.tv/directory/category/"), "slug"),
|
||||
output_field=models.TextField(),
|
||||
db_persist=True,
|
||||
)
|
||||
image_url = models.GeneratedField( # type: ignore # noqa: PGH003
|
||||
expression=Concat(
|
||||
Value("https://static-cdn.jtvnw.net/ttv-boxart/"),
|
||||
"id",
|
||||
Value("_IGDB.jpg"),
|
||||
),
|
||||
output_field=models.URLField(),
|
||||
db_persist=True,
|
||||
)
|
||||
display_name = models.TextField(blank=True, null=True)
|
||||
added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
|
||||
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
verbose_name: str = "Game"
|
||||
verbose_name_plural: str = "Games"
|
||||
ordering: tuple[Literal["display_name"]] = ("display_name",)
|
||||
image1_x_url = models.URLField(null=True, blank=True)
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.display_name or self.slug or self.id
|
||||
return self.image1_x_url or "Unknown"
|
||||
|
||||
|
||||
class Drop(auto_prefetch.Model):
|
||||
"""The actual drop that is being given out."""
|
||||
class Reward(models.Model):
|
||||
"""The actual reward you get when you complete the requirements.
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
created_at = models.DateTimeField(blank=True, null=True)
|
||||
entitlement_limit = models.IntegerField(blank=True, null=True)
|
||||
image_asset_url = models.URLField(blank=True, null=True)
|
||||
name = models.TextField(blank=True, null=True)
|
||||
added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
|
||||
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
|
||||
required_subs = models.IntegerField(blank=True, null=True)
|
||||
end_at = models.DateTimeField(blank=True, null=True)
|
||||
required_minutes_watched = models.IntegerField(blank=True, null=True)
|
||||
start_at = models.DateTimeField(blank=True, null=True)
|
||||
drop_campaign = auto_prefetch.ForeignKey("DropCampaign", on_delete=models.CASCADE, related_name="drops")
|
||||
Attributes:
|
||||
id (UUID): The primary key of the reward.
|
||||
name (str): The name of the reward.
|
||||
banner_image (Image): The banner image associated with the reward.
|
||||
thumbnail_image (Image): The thumbnail image associated with the reward.
|
||||
earnable_until (datetime): The date and time until the reward can be earned.
|
||||
redemption_instructions (str): Instructions on how to redeem the reward.
|
||||
redemption_url (str): URL for redeeming the reward.
|
||||
typename (str): The type name of the object, typically "Reward".
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
verbose_name: str = "Drop"
|
||||
verbose_name_plural: str = "Drops"
|
||||
ordering: tuple[Literal["name"]] = ("name",)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class DropCampaign(auto_prefetch.Model):
|
||||
"""Drops are grouped into campaigns.
|
||||
|
||||
For example, MultiVersus S1 Drops
|
||||
JSON example:
|
||||
{
|
||||
"id": "374628c6-34b4-11ef-a468-62ece0f03426",
|
||||
"name": "Twitchy Character Skin",
|
||||
"bannerImage": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"thumbnailImage": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"earnableUntil": "2024-07-30T06:59:59Z",
|
||||
"redemptionInstructions": "",
|
||||
"redemptionURL": "https://redeem.ubisoft.com/xdefiant/",
|
||||
"__typename": "Reward"
|
||||
}
|
||||
"""
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
account_link_url = models.URLField(blank=True, null=True)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
details_url = models.URLField(blank=True, null=True)
|
||||
end_at = models.DateTimeField(blank=True, null=True)
|
||||
image_url = models.URLField(blank=True, null=True)
|
||||
name = models.TextField(blank=True, null=True)
|
||||
start_at = models.DateTimeField(blank=True, null=True)
|
||||
status = models.TextField(blank=True, null=True)
|
||||
game = auto_prefetch.ForeignKey(
|
||||
Game,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="drop_campaigns",
|
||||
)
|
||||
added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
|
||||
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
verbose_name: str = "Drop Campaign"
|
||||
verbose_name_plural: str = "Drop Campaigns"
|
||||
ordering: tuple[Literal["name"]] = ("name",)
|
||||
id = models.UUIDField(primary_key=True)
|
||||
name = models.TextField(null=True, blank=True)
|
||||
banner_image = models.ForeignKey(Image, related_name="banner_rewards", on_delete=models.CASCADE, null=True)
|
||||
thumbnail_image = models.ForeignKey(Image, related_name="thumbnail_rewards", on_delete=models.CASCADE, null=True)
|
||||
earnable_until = models.DateTimeField(null=True)
|
||||
redemption_instructions = models.TextField(null=True, blank=True)
|
||||
redemption_url = models.URLField(null=True, blank=True)
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.game.display_name} - {self.name}"
|
||||
return self.name or "Unknown"
|
||||
|
||||
|
||||
class UnlockRequirements(models.Model):
|
||||
"""Requirements to unlock a reward.
|
||||
|
||||
Attributes:
|
||||
subs_goal (int): The number of subscriptions needed to unlock the reward.
|
||||
minute_watched_goal (int): The number of minutes watched needed to unlock the reward.
|
||||
typename (str): The type name of the object, typically "QuestRewardUnlockRequirements".
|
||||
|
||||
JSON example:
|
||||
{
|
||||
"subsGoal": 2,
|
||||
"minuteWatchedGoal": 0,
|
||||
"__typename": "QuestRewardUnlockRequirements"
|
||||
}
|
||||
"""
|
||||
|
||||
subs_goal = models.IntegerField(null=True)
|
||||
minute_watched_goal = models.IntegerField(null=True)
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.subs_goal} subs and {self.minute_watched_goal} minutes watched"
|
||||
|
||||
|
||||
class RewardCampaign(models.Model):
|
||||
"""Represents a reward campaign.
|
||||
|
||||
Attributes:
|
||||
id (UUID): The primary key of the reward campaign.
|
||||
name (str): The name of the reward campaign.
|
||||
brand (str): The brand associated with the campaign.
|
||||
starts_at (datetime): The start date and time of the campaign.
|
||||
ends_at (datetime): The end date and time of the campaign.
|
||||
status (str): The status of the campaign.
|
||||
summary (str): A brief summary of the campaign.
|
||||
instructions (str): Instructions for the campaign.
|
||||
external_url (str): The external URL related to the campaign.
|
||||
reward_value_url_param (str): URL parameter for the reward value.
|
||||
about_url (str): URL with more information about the campaign.
|
||||
is_sitewide (bool): Indicates if the campaign is sitewide.
|
||||
game (Game): The game associated with the campaign.
|
||||
image (Image): The image associated with the campaign.
|
||||
rewards (ManyToManyField): The rewards available in the campaign.
|
||||
typename (str): The type name of the object, typically "RewardCampaign".
|
||||
|
||||
JSON example:
|
||||
{
|
||||
"id": "3757a2ae-34b4-11ef-a468-62ece0f03426",
|
||||
"name": "XDefiant Season 1 Launch",
|
||||
"brand": "Ubisoft",
|
||||
"startsAt": "2024-07-02T17:00:00Z",
|
||||
"endsAt": "2024-07-30T06:59:59Z",
|
||||
"status": "UNKNOWN",
|
||||
"summary": "Get a redeemable code for the Twitchy Character Skin in XDefiant for gifting or purchasing 2 subscriptions of any tier to participating channels.",
|
||||
"instructions": "",
|
||||
"externalURL": "https://redeem.ubisoft.com/xdefiant/",
|
||||
"rewardValueURLParam": "",
|
||||
"aboutURL": "https://xdefiant.com/S1-twitch-rewards",
|
||||
"isSitewide": false,
|
||||
"game": {
|
||||
"id": "780302568",
|
||||
"slug": "xdefiant",
|
||||
"displayName": "XDefiant",
|
||||
"__typename": "Game"
|
||||
},
|
||||
"image": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"rewards": [
|
||||
{
|
||||
"id": "374628c6-34b4-11ef-a468-62ece0f03426",
|
||||
"name": "Twitchy Character Skin",
|
||||
"bannerImage": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"thumbnailImage": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"earnableUntil": "2024-07-30T06:59:59Z",
|
||||
"redemptionInstructions": "",
|
||||
"redemptionURL": "https://redeem.ubisoft.com/xdefiant/",
|
||||
"__typename": "Reward"
|
||||
}
|
||||
],
|
||||
"__typename": "RewardCampaign"
|
||||
}
|
||||
""" # noqa: E501
|
||||
|
||||
id = models.UUIDField(primary_key=True)
|
||||
name = models.TextField(null=True, blank=True)
|
||||
brand = models.TextField(null=True, blank=True)
|
||||
starts_at = models.DateTimeField(null=True)
|
||||
ends_at = models.DateTimeField(null=True)
|
||||
status = models.TextField(null=True, blank=True)
|
||||
summary = models.TextField(null=True, blank=True)
|
||||
instructions = models.TextField(null=True, blank=True)
|
||||
external_url = models.URLField(null=True, blank=True)
|
||||
reward_value_url_param = models.TextField(null=True, blank=True)
|
||||
about_url = models.URLField(null=True, blank=True)
|
||||
is_sitewide = models.BooleanField(null=True)
|
||||
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="reward_campaigns", null=True)
|
||||
image = models.ForeignKey(Image, on_delete=models.CASCADE, related_name="reward_campaigns", null=True)
|
||||
rewards = models.ManyToManyField(Reward, related_name="reward_campaigns")
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name or "Unknown"
|
||||
|
57
twitch_app/testboi.py
Normal file
57
twitch_app/testboi.py
Normal file
@ -0,0 +1,57 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@dataclass
|
||||
class Game:
|
||||
id: int
|
||||
slug: str
|
||||
display_name: str
|
||||
typename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Image:
|
||||
image1_x_url: str
|
||||
typename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Reward:
|
||||
id: UUID
|
||||
name: str
|
||||
banner_image: Image
|
||||
thumbnail_image: Image
|
||||
earnable_until: datetime
|
||||
redemption_instructions: str
|
||||
redemption_url: str
|
||||
typename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnlockRequirements:
|
||||
subs_goal: int
|
||||
minute_watched_goal: int
|
||||
typename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class RewardCampaign:
|
||||
id: UUID
|
||||
name: str
|
||||
brand: str
|
||||
starts_at: datetime
|
||||
ends_at: datetime
|
||||
status: str
|
||||
summary: str
|
||||
instructions: str
|
||||
external_url: str
|
||||
reward_value_url_param: str
|
||||
about_url: str
|
||||
is_sitewide: bool
|
||||
game: Game
|
||||
unlock_requirements: UnlockRequirements
|
||||
image: Image
|
||||
rewards: list[Reward]
|
||||
typename: str
|
Reference in New Issue
Block a user