WIP
This commit is contained in:
		
							
								
								
									
										0
									
								
								twitch_app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								twitch_app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										9
									
								
								twitch_app/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								twitch_app/admin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| from .models import DropBenefit, DropCampaign, Game, Organization, TimeBasedDrop | ||||
|  | ||||
| admin.site.register(DropBenefit) | ||||
| admin.site.register(DropCampaign) | ||||
| admin.site.register(Game) | ||||
| admin.site.register(Organization) | ||||
| admin.site.register(TimeBasedDrop) | ||||
							
								
								
									
										125
									
								
								twitch_app/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								twitch_app/api.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| import datetime | ||||
|  | ||||
| from django.db.models.manager import BaseManager | ||||
| from django.http import HttpRequest | ||||
| from ninja import Router, Schema | ||||
|  | ||||
| from .models import ( | ||||
|     DropBenefit, | ||||
|     DropCampaign, | ||||
|     Game, | ||||
|     Organization, | ||||
|     TimeBasedDrop, | ||||
| ) | ||||
|  | ||||
| router = Router( | ||||
|     tags=["twitch"], | ||||
| ) | ||||
|  | ||||
|  | ||||
| class OrganizationSchema(Schema): | ||||
|     id: str | None = None | ||||
|     name: str | None = None | ||||
|     added_at: datetime.datetime | None = None | ||||
|     modified_at: datetime.datetime | None = None | ||||
|  | ||||
|  | ||||
| class ChannelSchema(Schema): | ||||
|     id: str | ||||
|     display_name: str | None = None | ||||
|     name: str | None = None | ||||
|     added_at: datetime.datetime | None = None | ||||
|     modified_at: datetime.datetime | None = None | ||||
|  | ||||
|  | ||||
| class GameSchema(Schema): | ||||
|     id: str | ||||
|     slug: str | None = None | ||||
|     twitch_url: str | None = None | ||||
|     display_name: str | None = None | ||||
|     added_at: datetime.datetime | None = None | ||||
|     modified_at: datetime.datetime | None = None | ||||
|  | ||||
|  | ||||
| class DropBenefitSchema(Schema): | ||||
|     id: str | ||||
|     created_at: datetime.datetime | None = None | ||||
|     entitlement_limit: int | None = None | ||||
|     image_asset_url: str | None = None | ||||
|     is_ios_available: bool | None = None | ||||
|     name: str | None = None | ||||
|     owner_organization: OrganizationSchema | ||||
|     game: GameSchema | ||||
|     added_at: datetime.datetime | None = None | ||||
|     modified_at: datetime.datetime | None = None | ||||
|  | ||||
|  | ||||
| class TimeBasedDropSchema(Schema): | ||||
|     id: str | ||||
|     required_subs: int | None = None | ||||
|     end_at: datetime.datetime | None = None | ||||
|     name: str | None = None | ||||
|     required_minutes_watched: int | None = None | ||||
|     start_at: datetime.datetime | None = None | ||||
|     benefits: list[DropBenefitSchema] | ||||
|     added_at: datetime.datetime | None = None | ||||
|     modified_at: datetime.datetime | None = None | ||||
|  | ||||
|  | ||||
| class DropCampaignSchema(Schema): | ||||
|     id: str | ||||
|     account_link_url: str | None = None | ||||
|     description: str | None = None | ||||
|     details_url: str | None = None | ||||
|     end_at: datetime.datetime | None = None | ||||
|     image_url: str | None = None | ||||
|     name: str | None = None | ||||
|     start_at: datetime.datetime | None = None | ||||
|     status: str | None = None | ||||
|     game: GameSchema | None = None | ||||
|     owner: OrganizationSchema | None = None | ||||
|     channels: list[ChannelSchema] | None = None | ||||
|     time_based_drops: list[TimeBasedDropSchema] | None = None | ||||
|     added_at: datetime.datetime | None = None | ||||
|     modified_at: datetime.datetime | None = None | ||||
|  | ||||
|  | ||||
| # http://localhost:8000/api/twitch/organizations | ||||
| @router.get("/organizations", response=list[OrganizationSchema]) | ||||
| def get_organizations( | ||||
|     request: HttpRequest,  # noqa: ARG001 | ||||
| ) -> BaseManager[Organization]: | ||||
|     """Get all organizations.""" | ||||
|     return Organization.objects.all() | ||||
|  | ||||
|  | ||||
| # http://localhost:8000/api/twitch/games | ||||
| @router.get("/games", response=list[GameSchema]) | ||||
| def get_games(request: HttpRequest) -> BaseManager[Game]:  # noqa: ARG001 | ||||
|     """Get all games.""" | ||||
|     return Game.objects.all() | ||||
|  | ||||
|  | ||||
| # http://localhost:8000/api/twitch/drop_benefits | ||||
| @router.get("/drop_benefits", response=list[DropBenefitSchema]) | ||||
| def get_drop_benefits(request: HttpRequest) -> BaseManager[DropBenefit]:  # noqa: ARG001 | ||||
|     """Get all drop benefits.""" | ||||
|     return DropBenefit.objects.all() | ||||
|  | ||||
|  | ||||
| # http://localhost:8000/api/twitch/drop_campaigns | ||||
| @router.get("/drop_campaigns", response=list[DropCampaignSchema]) | ||||
| def get_drop_campaigns( | ||||
|     request: HttpRequest,  # noqa: ARG001 | ||||
| ) -> BaseManager[DropCampaign]: | ||||
|     """Get all drop campaigns.""" | ||||
|     return DropCampaign.objects.all() | ||||
|  | ||||
|  | ||||
| # http://localhost:8000/api/twitch/time_based_drops | ||||
| @router.get("/time_based_drops", response=list[TimeBasedDropSchema]) | ||||
| def get_time_based_drops( | ||||
|     request: HttpRequest,  # noqa: ARG001 | ||||
| ) -> BaseManager[TimeBasedDrop]: | ||||
|     """Get all time-based drops.""" | ||||
|     return TimeBasedDrop.objects.all() | ||||
							
								
								
									
										6
									
								
								twitch_app/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								twitch_app/apps.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class TwitchConfig(AppConfig): | ||||
|     default_auto_field: str = "django.db.models.BigAutoField" | ||||
|     name: str = "twitch_app" | ||||
							
								
								
									
										0
									
								
								twitch_app/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								twitch_app/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								twitch_app/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								twitch_app/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										245
									
								
								twitch_app/management/commands/scrape_twitch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								twitch_app/management/commands/scrape_twitch.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | ||||
| import asyncio | ||||
| import logging | ||||
| import typing | ||||
| from pathlib import Path | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from asgiref.sync import sync_to_async | ||||
| from django.core.management.base import BaseCommand | ||||
| 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 ( | ||||
|     DropBenefit, | ||||
|     DropCampaign, | ||||
|     Game, | ||||
|     Organization, | ||||
|     TimeBasedDrop, | ||||
|     User, | ||||
| ) | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from playwright.async_api._generated import BrowserContext, Page | ||||
|  | ||||
| # Where to store the Firefox profile | ||||
| data_dir = Path( | ||||
|     user_data_dir( | ||||
|         appname="TTVDrops", | ||||
|         appauthor="TheLovinator", | ||||
|         roaming=True, | ||||
|         ensure_exists=True, | ||||
|     ), | ||||
| ) | ||||
|  | ||||
| if not data_dir: | ||||
|     msg = "DATA_DIR is not set in settings.py" | ||||
|     raise ValueError(msg) | ||||
|  | ||||
| logger: logging.Logger = logging.getLogger("twitch.management.commands.scrape_twitch") | ||||
|  | ||||
|  | ||||
| async def insert_data(data: dict) -> None:  # noqa: PLR0914, C901 | ||||
|     """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_id = user_data["id"] | ||||
|     drop_campaign_data = user_data["dropCampaign"] | ||||
|     if not drop_campaign_data: | ||||
|         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) | ||||
|  | ||||
|     # 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"], | ||||
|         }, | ||||
|     ) | ||||
|     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["accountLinkURL"], | ||||
|             "description": drop_campaign_data["description"], | ||||
|             "details_url": drop_campaign_data["detailsURL"], | ||||
|             "end_at": drop_campaign_data["endAt"], | ||||
|             "image_url": drop_campaign_data["imageURL"], | ||||
|             "name": drop_campaign_data["name"], | ||||
|             "start_at": drop_campaign_data["startAt"], | ||||
|             "status": drop_campaign_data["status"], | ||||
|             "game": game, | ||||
|             "owner": owner, | ||||
|         }, | ||||
|     ) | ||||
|     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"] | ||||
|         drop_benefits = [] | ||||
|  | ||||
|         for edge in drop_benefit_edges: | ||||
|             benefit_data = edge["benefit"] | ||||
|             benefit_owner_data = benefit_data["ownerOrganization"] | ||||
|  | ||||
|             benefit_owner, 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("Benefit owner created: %s", benefit_owner) | ||||
|  | ||||
|             benefit_game_data = benefit_data["game"] | ||||
|             benefit_game, created = await sync_to_async(Game.objects.get_or_create)( | ||||
|                 id=benefit_game_data["id"], | ||||
|                 defaults={"name": benefit_game_data["name"]}, | ||||
|             ) | ||||
|             if created: | ||||
|                 logger.debug("Benefit game created: %s", benefit_game) | ||||
|  | ||||
|             benefit, created = await sync_to_async(DropBenefit.objects.get_or_create)( | ||||
|                 id=benefit_data["id"], | ||||
|                 defaults={ | ||||
|                     "created_at": benefit_data["createdAt"], | ||||
|                     "entitlement_limit": benefit_data["entitlementLimit"], | ||||
|                     "image_asset_url": benefit_data["imageAssetURL"], | ||||
|                     "is_ios_available": benefit_data["isIosAvailable"], | ||||
|                     "name": benefit_data["name"], | ||||
|                     "owner_organization": benefit_owner, | ||||
|                     "game": benefit_game, | ||||
|                 }, | ||||
|             ) | ||||
|             drop_benefits.append(benefit) | ||||
|             if created: | ||||
|                 logger.debug("Benefit created: %s", benefit) | ||||
|  | ||||
|         time_based_drop, created = await sync_to_async( | ||||
|             TimeBasedDrop.objects.get_or_create, | ||||
|         )( | ||||
|             id=drop_data["id"], | ||||
|             defaults={ | ||||
|                 "required_subs": drop_data["requiredSubs"], | ||||
|                 "end_at": drop_data["endAt"], | ||||
|                 "name": drop_data["name"], | ||||
|                 "required_minutes_watched": drop_data["requiredMinutesWatched"], | ||||
|                 "start_at": drop_data["startAt"], | ||||
|             }, | ||||
|         ) | ||||
|         await sync_to_async(time_based_drop.benefits.set)(drop_benefits) | ||||
|         await sync_to_async(drop_campaign.time_based_drops.add)(time_based_drop) | ||||
|  | ||||
|         if created: | ||||
|             logger.debug("Time-based drop created: %s", time_based_drop) | ||||
|  | ||||
|     # Create or get the user | ||||
|     user, created = await sync_to_async(User.objects.get_or_create)(id=user_id) | ||||
|     await sync_to_async(user.drop_campaigns.add)(drop_campaign) | ||||
|     if created: | ||||
|         logger.debug("User created: %s", user) | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Scrape Twitch Drops Campaigns with login using Firefox" | ||||
|  | ||||
|     async def run(  # noqa: PLR6301, C901 | ||||
|         self, | ||||
|         playwright: Playwright, | ||||
|     ) -> list[dict[str, typing.Any]]: | ||||
|         profile_dir: Path = Path(data_dir / "firefox-profile") | ||||
|         profile_dir.mkdir(parents=True, exist_ok=True) | ||||
|         logger.debug( | ||||
|             "Launching Firefox browser with user data directory: %s", | ||||
|             profile_dir, | ||||
|         ) | ||||
|  | ||||
|         browser: BrowserContext = await playwright.firefox.launch_persistent_context( | ||||
|             user_data_dir=profile_dir, | ||||
|             headless=True, | ||||
|         ) | ||||
|         logger.debug("Launched Firefox browser") | ||||
|  | ||||
|         page: Page = await browser.new_page() | ||||
|         json_data: list[dict] = [] | ||||
|  | ||||
|         async def handle_response(response: Response) -> None: | ||||
|             if "https://gql.twitch.tv/gql" in response.url: | ||||
|                 try: | ||||
|                     body: typing.Any = await response.json() | ||||
|                     json_data.extend(body) | ||||
|                 except Exception: | ||||
|                     logger.exception( | ||||
|                         "Failed to parse JSON from %s", | ||||
|                         response.url, | ||||
|                     ) | ||||
|  | ||||
|         page.on("response", handle_response) | ||||
|         await page.goto("https://www.twitch.tv/drops/campaigns") | ||||
|         logger.debug("Navigated to Twitch drops campaigns page") | ||||
|  | ||||
|         logged_in = False | ||||
|         while not logged_in: | ||||
|             try: | ||||
|                 await page.wait_for_selector( | ||||
|                     'div[data-a-target="top-nav-avatar"]', | ||||
|                     timeout=30000, | ||||
|                 ) | ||||
|                 logged_in = True | ||||
|                 logger.info("Logged in to Twitch") | ||||
|             except KeyboardInterrupt as e: | ||||
|                 raise KeyboardInterrupt from e | ||||
|             except Exception:  # noqa: BLE001 | ||||
|                 await asyncio.sleep(5) | ||||
|                 logger.info("Waiting for login") | ||||
|  | ||||
|         await page.wait_for_load_state("networkidle") | ||||
|         logger.debug("Page loaded. Scraping data...") | ||||
|  | ||||
|         await browser.close() | ||||
|  | ||||
|         for num, campaign in enumerate(json_data, start=1): | ||||
|             logger.info("Processing JSON %d of %d", num, len(json_data)) | ||||
|             if not isinstance(campaign, dict): | ||||
|                 continue | ||||
|  | ||||
|             if "dropCampaign" in campaign.get("data", {}).get("user", {}): | ||||
|                 await insert_data(campaign) | ||||
|  | ||||
|             if "dropCampaigns" in campaign.get("data", {}).get("user", {}): | ||||
|                 await insert_data(campaign) | ||||
|  | ||||
|         return json_data | ||||
|  | ||||
|     def handle(self, *args, **kwargs) -> None:  # noqa: ANN002, ARG002, ANN003 | ||||
|         asyncio.run(self.run_with_playwright()) | ||||
|  | ||||
|     async def run_with_playwright(self) -> None: | ||||
|         async with async_playwright() as playwright: | ||||
|             await self.run(playwright) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     Command().handle() | ||||
							
								
								
									
										137
									
								
								twitch_app/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								twitch_app/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| # Generated by Django 5.0.6 on 2024-07-01 00:08 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| import django.db.models.functions.text | ||||
| from django.db import migrations, models | ||||
| from django.db.migrations.operations.base import Operation | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     initial = True | ||||
|  | ||||
|     dependencies: list[tuple[str, str]] = [] | ||||
|  | ||||
|     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(), | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("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)), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="Organization", | ||||
|             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)), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="DropBenefit", | ||||
|             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)), | ||||
|                 ("is_ios_available", models.BooleanField(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)), | ||||
|                 ( | ||||
|                     "game", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         to="twitch_app.game", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "owner_organization", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         to="twitch_app.organization", | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="TimeBasedDrop", | ||||
|             fields=[ | ||||
|                 ("id", models.TextField(primary_key=True, serialize=False)), | ||||
|                 ("required_subs", models.IntegerField(blank=True, null=True)), | ||||
|                 ("end_at", models.DateTimeField(blank=True, null=True)), | ||||
|                 ("name", models.TextField(blank=True, null=True)), | ||||
|                 ( | ||||
|                     "required_minutes_watched", | ||||
|                     models.IntegerField(blank=True, null=True), | ||||
|                 ), | ||||
|                 ("start_at", models.DateTimeField(blank=True, null=True)), | ||||
|                 ("added_at", models.DateTimeField(auto_now_add=True, null=True)), | ||||
|                 ("modified_at", models.DateTimeField(auto_now=True, null=True)), | ||||
|                 ("benefits", models.ManyToManyField(to="twitch_app.dropbenefit")), | ||||
|             ], | ||||
|         ), | ||||
|         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)), | ||||
|                 ( | ||||
|                     "game", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         related_name="drop_campaigns", | ||||
|                         to="twitch_app.game", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "owner", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         related_name="drop_campaigns", | ||||
|                         to="twitch_app.organization", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "time_based_drops", | ||||
|                     models.ManyToManyField(to="twitch_app.timebaseddrop"), | ||||
|                 ), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="User", | ||||
|             fields=[ | ||||
|                 ("id", models.TextField(primary_key=True, serialize=False)), | ||||
|                 ("added_at", models.DateTimeField(auto_now_add=True, null=True)), | ||||
|                 ("modified_at", models.DateTimeField(auto_now=True, null=True)), | ||||
|                 ( | ||||
|                     "drop_campaigns", | ||||
|                     models.ManyToManyField(to="twitch_app.dropcampaign"), | ||||
|                 ), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										27
									
								
								twitch_app/migrations/0002_game_image_url.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								twitch_app/migrations/0002_game_image_url.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| # Generated by Django 5.0.6 on 2024-07-01 03:49 | ||||
|  | ||||
| import django.db.models.functions.text | ||||
| from django.db import migrations, models | ||||
| from django.db.migrations.operations.base import Operation | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies: list[tuple[str, str]] = [ | ||||
|         ("twitch_app", "0001_initial"), | ||||
|     ] | ||||
|  | ||||
|     operations: list[Operation] = [ | ||||
|         migrations.AddField( | ||||
|             model_name="game", | ||||
|             name="image_url", | ||||
|             field=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(), | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								twitch_app/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								twitch_app/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										109
									
								
								twitch_app/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								twitch_app/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| from django.db import models | ||||
| from django.db.models import Value | ||||
| from django.db.models.functions import ( | ||||
|     Concat, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class Organization(models.Model): | ||||
|     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) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.name or self.id | ||||
|  | ||||
|  | ||||
| class Game(models.Model): | ||||
|     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) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.display_name or self.slug or self.id | ||||
|  | ||||
|  | ||||
| class DropBenefit(models.Model): | ||||
|     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) | ||||
|     is_ios_available = models.BooleanField(blank=True, null=True) | ||||
|     name = models.TextField(blank=True, null=True) | ||||
|     owner_organization = models.ForeignKey(Organization, on_delete=models.CASCADE) | ||||
|     game = models.ForeignKey(Game, on_delete=models.CASCADE) | ||||
|     added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True) | ||||
|     modified_at = models.DateTimeField(blank=True, null=True, auto_now=True) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.name or self.id | ||||
|  | ||||
|  | ||||
| class TimeBasedDrop(models.Model): | ||||
|     id = models.TextField(primary_key=True) | ||||
|     required_subs = models.IntegerField(blank=True, null=True) | ||||
|     end_at = models.DateTimeField(blank=True, null=True) | ||||
|     name = models.TextField(blank=True, null=True) | ||||
|     required_minutes_watched = models.IntegerField(blank=True, null=True) | ||||
|     start_at = models.DateTimeField(blank=True, null=True) | ||||
|     benefits = models.ManyToManyField(DropBenefit) | ||||
|     added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True) | ||||
|     modified_at = models.DateTimeField(blank=True, null=True, auto_now=True) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.name or self.id | ||||
|  | ||||
|  | ||||
| class DropCampaign(models.Model): | ||||
|     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 = models.ForeignKey( | ||||
|         Game, | ||||
|         on_delete=models.CASCADE, | ||||
|         related_name="drop_campaigns", | ||||
|     ) | ||||
|     owner = models.ForeignKey( | ||||
|         Organization, | ||||
|         on_delete=models.CASCADE, | ||||
|         related_name="drop_campaigns", | ||||
|     ) | ||||
|     time_based_drops = models.ManyToManyField(TimeBasedDrop) | ||||
|     added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True) | ||||
|     modified_at = models.DateTimeField(blank=True, null=True, auto_now=True) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.name or self.id | ||||
|  | ||||
|  | ||||
| class User(models.Model): | ||||
|     id = models.TextField(primary_key=True) | ||||
|     drop_campaigns = models.ManyToManyField(DropCampaign) | ||||
|     added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True) | ||||
|     modified_at = models.DateTimeField(blank=True, null=True, auto_now=True) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.id | ||||
							
								
								
									
										10
									
								
								twitch_app/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								twitch_app/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from django.urls import URLPattern | ||||
|  | ||||
| app_name: str = "twitch" | ||||
|  | ||||
| urlpatterns: list[URLPattern] = [] | ||||
		Reference in New Issue
	
	Block a user