142 lines
6.6 KiB
Python
142 lines
6.6 KiB
Python
import requests
|
|
import time
|
|
from django.core.management.base import BaseCommand
|
|
from django.utils.dateparse import parse_date
|
|
from store.models import Game, Set, Card
|
|
|
|
class Command(BaseCommand):
|
|
help = 'Populates the database with Pokémon TCG sets and cards using the TCGDex REST API (English).'
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
'--clear',
|
|
action='store_true',
|
|
help='Clear existing Pokémon TCG cards and sets before populating.'
|
|
)
|
|
parser.add_argument(
|
|
'--duration',
|
|
default='7',
|
|
help='(Not full supported by TCGDex) Duration in days to look back. For now, this will just fetch all sets as TCGDex sets endpoint is not sorted by date.'
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
self.stdout.write(self.style.SUCCESS('Starting Pokémon TCG population (via TCGDex)...'))
|
|
|
|
# User Agent is good practice
|
|
self.headers = {'User-Agent': 'ExampleTCGSite/1.0'}
|
|
base_url = "https://api.tcgdex.net/v2/en"
|
|
|
|
# 1. Ensure Game exists
|
|
game, created = Game.objects.get_or_create(
|
|
name="Pokémon Trading Card Game",
|
|
defaults={'slug': 'pokemon-tcg'}
|
|
)
|
|
if created:
|
|
self.stdout.write(self.style.SUCCESS(f'Created Game: {game.name}'))
|
|
else:
|
|
self.stdout.write(f'Found Game: {game.name}')
|
|
|
|
# Handle --clear
|
|
if options['clear']:
|
|
self.stdout.write(self.style.WARNING('Clearing existing Pokémon data...'))
|
|
Card.objects.filter(set__game=game).delete()
|
|
Set.objects.filter(game=game).delete()
|
|
self.stdout.write(self.style.SUCCESS('Cleared Pokémon data.'))
|
|
|
|
# 2. Fetch Sets
|
|
self.stdout.write('Fetching sets from TCGDex...')
|
|
|
|
try:
|
|
# TCGDex /sets returns a list of minimal set objects
|
|
response = requests.get(f"{base_url}/sets", headers=self.headers)
|
|
response.raise_for_status()
|
|
sets_data = response.json()
|
|
except Exception as e:
|
|
self.stdout.write(self.style.ERROR(f'Failed to fetch sets: {e}'))
|
|
return
|
|
|
|
self.stdout.write(f'Found {len(sets_data)} sets. Processing...')
|
|
|
|
processed_sets = []
|
|
for s_data in sets_data:
|
|
# s_data example: {"id": "base1", "name": "Base Set", ...}
|
|
# TCGDex sets don't consistently provide releaseDate in the list view,
|
|
# so we'll leave it null or updated if we fetched details (which we might do for cards).
|
|
# For efficiency we might not fetch set details just for date if unnecessary.
|
|
|
|
set_obj, created = Set.objects.update_or_create(
|
|
code=s_data.get('id'),
|
|
game=game,
|
|
defaults={
|
|
'name': s_data.get('name'),
|
|
# 'release_date': None # Not available in simple list
|
|
}
|
|
)
|
|
processed_sets.append(set_obj)
|
|
|
|
self.stdout.write(self.style.SUCCESS(f'Processed {len(processed_sets)} sets.'))
|
|
|
|
# 3. Fetch Cards
|
|
self.stdout.write('Fetching cards...')
|
|
|
|
# We must iterate sets to get cards, as there isn't a robust "all cards new" stream without pagination headaches
|
|
# on some APIs, and TCGDex structure favors set traversal.
|
|
|
|
total_sets = len(processed_sets)
|
|
for idx, set_obj in enumerate(processed_sets):
|
|
self.stdout.write(f' [{idx+1}/{total_sets}] Fetching cards for set: {set_obj.name} ({set_obj.code})...')
|
|
|
|
try:
|
|
# Fetch Set Detail to get cards
|
|
# Endpoint: /sets/{id}
|
|
set_resp = requests.get(f"{base_url}/sets/{set_obj.code}", headers=self.headers)
|
|
if set_resp.status_code == 404:
|
|
self.stdout.write(self.style.WARNING(f' Set {set_obj.code} detail not found. Skipping.'))
|
|
continue
|
|
set_resp.raise_for_status()
|
|
|
|
set_detail = set_resp.json()
|
|
cards = set_detail.get('cards', [])
|
|
|
|
except Exception as e:
|
|
self.stdout.write(self.style.ERROR(f' Failed to fetch cards for {set_obj.name}: {e}'))
|
|
continue
|
|
|
|
self.stdout.write(f' Found {len(cards)} cards.')
|
|
|
|
for c_data in cards:
|
|
# c_data example: {"id": "base1-1", "localId": "1", "name": "Alakazam", "image": "..."}
|
|
# Rarity is NOT in this list usually, requires fetching card detail. Skipping for speed.
|
|
|
|
# Image URL: TCGDex gives a base URL usually, e.g. ".../base1/1"
|
|
# Sometimes it has /high.png or /low.png supported. The provided 'image' field often works as is.
|
|
# It might have extension like .png or just be the base.
|
|
# The user-provided example curl showed "image": "https://assets.tcgdex.net/en/base/base1/1"
|
|
# Those usually redirect to an image or handle extension. Let's append /high.png if we want best quality or try as is.
|
|
# Actually, TCGDex assets usually need an extension. Let's assume the API provides a valid URL or we append.
|
|
# Inspecting typical TCGDex response: "image": ".../1" (no extension).
|
|
# Browsers handle it, but for our backend saving it might be tricky if it's not a direct file.
|
|
# Let's save the URL as provided + "/high.png" as a guess for better quality if it doesn't have extension,
|
|
# Or just use the provided one.
|
|
# Update: TCGDex documentation often says: {image}/high.webp or {image}/low.webp
|
|
|
|
base_image = c_data.get('image')
|
|
image_url = f"{base_image}/high.webp" if base_image else ''
|
|
|
|
Card.objects.update_or_create(
|
|
scryfall_id=c_data.get('id'),
|
|
defaults={
|
|
'set': set_obj,
|
|
'name': c_data.get('name'),
|
|
'rarity': '', # specific call needed, simplifying
|
|
'image_url': image_url,
|
|
'collector_number': c_data.get('localId', ''),
|
|
'external_url': f"https://tcgdex.dev/cards/{c_data.get('id')}", # simplified assumption
|
|
}
|
|
)
|
|
|
|
# Rate limiting check - TCGDex is generous but good validation to not slam
|
|
# time.sleep(0.1)
|
|
|
|
self.stdout.write(self.style.SUCCESS('Finished Pokémon TCG population!'))
|