🎉 Initialize module repository
This commit is contained in:
801
models/mvd_tcg_card.py
Normal file
801
models/mvd_tcg_card.py
Normal file
@@ -0,0 +1,801 @@
|
||||
"""Scryfall-specific MTG card import helpers for the MTG connector."""
|
||||
|
||||
import re
|
||||
|
||||
from odoo import _, Command, api, fields, models
|
||||
|
||||
KEYWORD_CODE_PATTERN = re.compile(r"[^a-z0-9]+")
|
||||
|
||||
|
||||
class MvdTcgCard(models.Model):
|
||||
"""Extend MTG card references with Scryfall connector metadata."""
|
||||
|
||||
_inherit = "mvd.tcg.card"
|
||||
|
||||
mtg_scryfall_id = fields.Char(string="Scryfall ID", index=True)
|
||||
mtg_scryfall_uri = fields.Char(string="Scryfall URL", copy=False, readonly=True)
|
||||
mtg_scryfall_last_import_run_id = fields.Many2one(
|
||||
"mvd.tcg.mtg.scryfall.import.run",
|
||||
string="Last Import Run",
|
||||
copy=False,
|
||||
index=True,
|
||||
ondelete="set null",
|
||||
readonly=True,
|
||||
)
|
||||
mtg_scryfall_last_synced_at = fields.Datetime(
|
||||
string="Last Synced At",
|
||||
copy=False,
|
||||
readonly=True,
|
||||
)
|
||||
mtg_scryfall_image_ids = fields.One2many(
|
||||
"mvd.tcg.mtg.card.image",
|
||||
"card_id",
|
||||
string="Localized Images",
|
||||
copy=False,
|
||||
readonly=True,
|
||||
)
|
||||
mtg_display_image_1920 = fields.Image(
|
||||
string="Localized Image",
|
||||
compute="_compute_mtg_display_images",
|
||||
max_width=1920,
|
||||
max_height=1920,
|
||||
)
|
||||
mtg_display_image_512 = fields.Image(
|
||||
string="Localized Preview Image",
|
||||
compute="_compute_mtg_display_images",
|
||||
max_width=512,
|
||||
max_height=512,
|
||||
)
|
||||
|
||||
def _mvd_tcg_get_deck_image_binary(self):
|
||||
"""Prefer localized display images for deck previews.
|
||||
|
||||
Returns:
|
||||
str | bool: Base64 image data for deck-related previews.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return (
|
||||
self.mtg_display_image_512
|
||||
or self.mtg_display_image_1920
|
||||
or super()._mvd_tcg_get_deck_image_binary()
|
||||
)
|
||||
|
||||
def _mtg_scryfall_get_refresh_query(self):
|
||||
"""Return the preferred lookup query for a Scryfall refresh.
|
||||
|
||||
Returns:
|
||||
str | bool: URL or identifier reusable by the shared lookup parser.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.mtg_scryfall_uri:
|
||||
return self.mtg_scryfall_uri
|
||||
if self.mtg_scryfall_id:
|
||||
return self.mtg_scryfall_id
|
||||
if self.mtg_set_code and self.mtg_collector_number:
|
||||
return (
|
||||
f"https://api.scryfall.com/cards/"
|
||||
f"{self.mtg_set_code}/{self.mtg_collector_number}"
|
||||
)
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_build_external_ref(self, payload):
|
||||
"""Build the stable MTG print reference used by the connector.
|
||||
|
||||
Args:
|
||||
payload: Raw Scryfall card payload.
|
||||
|
||||
Returns:
|
||||
str: Stable reference in ``set_code:collector_number`` form.
|
||||
"""
|
||||
set_code = (payload.get("set") or "").strip().lower()
|
||||
collector_number = (payload.get("collector_number") or "").strip().lower()
|
||||
return f"{set_code}:{collector_number}"
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_select_primary_payload(self, payloads):
|
||||
"""Return the payload that should drive primary MTG field values.
|
||||
|
||||
Args:
|
||||
payloads: Raw localized Scryfall payloads for one print group.
|
||||
|
||||
Returns:
|
||||
dict: Primary payload, preferring English when available.
|
||||
"""
|
||||
valid_payloads = [
|
||||
payload
|
||||
for payload in (payloads or [])
|
||||
if payload.get("object") == "card" and payload.get("id")
|
||||
]
|
||||
if not valid_payloads:
|
||||
return {}
|
||||
|
||||
english_payload = next(
|
||||
(
|
||||
payload
|
||||
for payload in valid_payloads
|
||||
if (payload.get("lang") or "").strip().lower() == "en"
|
||||
),
|
||||
False,
|
||||
)
|
||||
return english_payload or valid_payloads[0]
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_extract_image_url(self, payload):
|
||||
"""Return the best image URL available for a Scryfall payload.
|
||||
|
||||
Args:
|
||||
payload: Raw Scryfall card payload.
|
||||
|
||||
Returns:
|
||||
str | bool: Image URL or ``False`` when none is available.
|
||||
"""
|
||||
image_uris = payload.get("image_uris") or {}
|
||||
if image_uris.get("normal"):
|
||||
return image_uris["normal"]
|
||||
|
||||
for face_payload in payload.get("card_faces") or []:
|
||||
face_image_uris = face_payload.get("image_uris") or {}
|
||||
if face_image_uris.get("normal"):
|
||||
return face_image_uris["normal"]
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_load_group_image_base64(self, payloads):
|
||||
"""Load the best available card image for one localized print group.
|
||||
|
||||
Args:
|
||||
payloads: Raw localized Scryfall payloads for one print group.
|
||||
|
||||
Returns:
|
||||
str | bool: Base64-encoded image data or ``False`` when unavailable.
|
||||
"""
|
||||
scryfall_api = self.env["mvd.tcg.mtg.scryfall.api"]
|
||||
for payload in payloads or ():
|
||||
image_base64 = scryfall_api.load_image_base64(
|
||||
self._mtg_scryfall_extract_image_url(payload)
|
||||
)
|
||||
if image_base64:
|
||||
return image_base64
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_collect_group_images(self, payloads):
|
||||
"""Collect localized imported images for one Scryfall print group.
|
||||
|
||||
Args:
|
||||
payloads: Raw localized Scryfall payloads for one print group.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Base64 image data keyed by Scryfall language code.
|
||||
"""
|
||||
scryfall_api = self.env["mvd.tcg.mtg.scryfall.api"]
|
||||
collected_images = {}
|
||||
for payload in payloads or ():
|
||||
language_code = (payload.get("lang") or "").strip().lower() or "en"
|
||||
if language_code in collected_images:
|
||||
continue
|
||||
image_base64 = scryfall_api.load_image_base64(
|
||||
self._mtg_scryfall_extract_image_url(payload)
|
||||
)
|
||||
if image_base64:
|
||||
collected_images[language_code] = image_base64
|
||||
return collected_images
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_extract_card_type_codes(self, payload):
|
||||
"""Extract normalized MTG card type codes from a Scryfall payload.
|
||||
|
||||
Args:
|
||||
payload: Raw Scryfall card payload.
|
||||
|
||||
Returns:
|
||||
list[str]: Known MTG card type codes present on the card.
|
||||
"""
|
||||
known_codes = {
|
||||
card_type.code: card_type.code
|
||||
for card_type in self.env["mvd.tcg.mtg.card.type"].search([])
|
||||
}
|
||||
raw_type_values = [payload.get("type_line")]
|
||||
raw_type_values.extend(
|
||||
face_payload.get("type_line")
|
||||
for face_payload in payload.get("card_faces") or []
|
||||
)
|
||||
extracted_codes = []
|
||||
for raw_type_value in raw_type_values:
|
||||
normalized_value = (raw_type_value or "").replace("—", " ")
|
||||
for token in normalized_value.split():
|
||||
type_code = token.strip().lower()
|
||||
if type_code in known_codes and type_code not in extracted_codes:
|
||||
extracted_codes.append(type_code)
|
||||
return extracted_codes
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_extract_face_payloads(self, payload):
|
||||
"""Return ordered card-face payloads from one Scryfall payload.
|
||||
|
||||
Args:
|
||||
payload: Raw Scryfall card payload.
|
||||
|
||||
Returns:
|
||||
list[dict]: Ordered face payloads, or an empty list for single-face cards.
|
||||
"""
|
||||
return [
|
||||
face_payload
|
||||
for face_payload in (payload.get("card_faces") or [])
|
||||
if isinstance(face_payload, dict)
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_normalize_keyword_code(self, keyword_name):
|
||||
"""Build a stable code for one MTG keyword.
|
||||
|
||||
Args:
|
||||
keyword_name: Human keyword such as ``Double strike``.
|
||||
|
||||
Returns:
|
||||
str: Normalized keyword code.
|
||||
"""
|
||||
normalized_keyword = KEYWORD_CODE_PATTERN.sub(
|
||||
"-",
|
||||
(keyword_name or "").strip().lower(),
|
||||
).strip("-")
|
||||
return normalized_keyword or False
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_resolve_taxonomy_records(
|
||||
self,
|
||||
model_name,
|
||||
codes,
|
||||
*,
|
||||
display_names=None,
|
||||
create_missing=False,
|
||||
):
|
||||
"""Resolve normalized taxonomy records from codes.
|
||||
|
||||
Args:
|
||||
model_name: Technical taxonomy model name.
|
||||
codes: Raw taxonomy codes.
|
||||
display_names: Optional display names keyed by normalized code.
|
||||
create_missing: Whether unknown codes should be created.
|
||||
|
||||
Returns:
|
||||
Model: Matching taxonomy records.
|
||||
"""
|
||||
normalized_codes = []
|
||||
for code in codes or []:
|
||||
normalized_code = (code or "").strip().lower()
|
||||
if normalized_code and normalized_code not in normalized_codes:
|
||||
normalized_codes.append(normalized_code)
|
||||
if not normalized_codes:
|
||||
return self.env[model_name]
|
||||
|
||||
taxonomy_model = self.env[model_name]
|
||||
taxonomy_records = taxonomy_model.search([("code", "in", normalized_codes)])
|
||||
records_by_code = {record.code: record for record in taxonomy_records}
|
||||
if create_missing:
|
||||
for normalized_code in normalized_codes:
|
||||
if normalized_code in records_by_code:
|
||||
continue
|
||||
display_name = (display_names or {}).get(normalized_code) or normalized_code
|
||||
taxonomy_record = taxonomy_model.create(
|
||||
{
|
||||
"name": display_name,
|
||||
"code": normalized_code,
|
||||
}
|
||||
)
|
||||
records_by_code[normalized_code] = taxonomy_record
|
||||
return taxonomy_model.browse(
|
||||
[records_by_code[code].id for code in normalized_codes if code in records_by_code]
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_map_translation_language_codes(self, scryfall_language_code):
|
||||
"""Map one Scryfall language code to active Odoo language codes.
|
||||
|
||||
Args:
|
||||
scryfall_language_code: Raw Scryfall language code such as ``de``.
|
||||
|
||||
Returns:
|
||||
list[str]: Active Odoo language codes for translation updates.
|
||||
"""
|
||||
return self.env["mvd.tcg.mtg.scryfall.api"].map_translation_language_codes(
|
||||
scryfall_language_code
|
||||
)
|
||||
|
||||
def _mtg_scryfall_update_translations_from_payload(self, payload):
|
||||
"""Update translated MTG card fields from one localized payload.
|
||||
|
||||
Args:
|
||||
payload: Raw localized Scryfall card payload.
|
||||
|
||||
Returns:
|
||||
None: The record is updated in place.
|
||||
"""
|
||||
self.ensure_one()
|
||||
card = self.with_context(
|
||||
mvd_tcg_bypass_validated_write=True,
|
||||
mvd_tcg_bypass_external_ref_write=True,
|
||||
)
|
||||
|
||||
language_codes = card._mtg_scryfall_map_translation_language_codes(
|
||||
payload.get("lang")
|
||||
)
|
||||
if not language_codes:
|
||||
return
|
||||
|
||||
translated_values = {
|
||||
"name": payload.get("printed_name") or payload.get("name") or False,
|
||||
"mtg_type_line": payload.get("printed_type_line") or payload.get("type_line") or False,
|
||||
"mtg_oracle_text": payload.get("printed_text") or payload.get("oracle_text") or False,
|
||||
"mtg_flavor_text": payload.get("flavor_text") or False,
|
||||
}
|
||||
for field_name, field_value in translated_values.items():
|
||||
if field_value:
|
||||
card.update_field_translations(
|
||||
field_name,
|
||||
{
|
||||
language_code: field_value
|
||||
for language_code in language_codes
|
||||
},
|
||||
)
|
||||
|
||||
def _mtg_scryfall_update_face_translations_from_payload(self, payload):
|
||||
"""Update translated face fields from one localized Scryfall payload.
|
||||
|
||||
Args:
|
||||
payload: Raw localized Scryfall card payload.
|
||||
|
||||
Returns:
|
||||
None: The record is updated in place.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
language_codes = self._mtg_scryfall_map_translation_language_codes(
|
||||
payload.get("lang")
|
||||
)
|
||||
if not language_codes:
|
||||
return
|
||||
|
||||
localized_faces = self._mtg_scryfall_extract_face_payloads(payload)
|
||||
if not localized_faces:
|
||||
return
|
||||
|
||||
faces_by_sequence = {face.sequence: face for face in self.mtg_face_ids}
|
||||
for index, face_payload in enumerate(localized_faces, start=1):
|
||||
face = faces_by_sequence.get(index)
|
||||
if not face:
|
||||
continue
|
||||
translated_values = {
|
||||
"name": face_payload.get("printed_name") or face_payload.get("name") or False,
|
||||
"type_line": face_payload.get("printed_type_line") or face_payload.get("type_line") or False,
|
||||
"oracle_text": face_payload.get("printed_text") or face_payload.get("oracle_text") or False,
|
||||
"flavor_text": face_payload.get("flavor_text") or False,
|
||||
}
|
||||
for field_name, field_value in translated_values.items():
|
||||
if field_value:
|
||||
face.with_context(
|
||||
mvd_tcg_bypass_validated_write=True,
|
||||
mvd_tcg_bypass_external_ref_write=True,
|
||||
).update_field_translations(
|
||||
field_name,
|
||||
{
|
||||
language_code: field_value
|
||||
for language_code in language_codes
|
||||
},
|
||||
)
|
||||
|
||||
def _mtg_scryfall_get_preferred_image_language_codes(self):
|
||||
"""Return preferred Scryfall image languages for the current UI context.
|
||||
|
||||
Returns:
|
||||
list[str]: Ordered Scryfall language codes with fallbacks.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
scryfall_api = self.env["mvd.tcg.mtg.scryfall.api"]
|
||||
current_odoo_language_code = self.env.context.get("lang") or self.env.user.lang
|
||||
language_codes = []
|
||||
|
||||
preferred_scryfall_code = scryfall_api.map_scryfall_language_code(
|
||||
current_odoo_language_code
|
||||
)
|
||||
if preferred_scryfall_code:
|
||||
language_codes.append(preferred_scryfall_code)
|
||||
if "en" not in language_codes:
|
||||
language_codes.append("en")
|
||||
return language_codes
|
||||
|
||||
def _mtg_scryfall_sync_localized_images(self, localized_images):
|
||||
"""Create or update localized imported images for the current card.
|
||||
|
||||
Args:
|
||||
localized_images: Base64 image data keyed by Scryfall language code.
|
||||
|
||||
Returns:
|
||||
None: The record is updated in place.
|
||||
"""
|
||||
self.ensure_one()
|
||||
card = self.with_context(
|
||||
mvd_tcg_bypass_validated_write=True,
|
||||
mvd_tcg_bypass_external_ref_write=True,
|
||||
)
|
||||
|
||||
image_model = self.env["mvd.tcg.mtg.card.image"]
|
||||
existing_images = {
|
||||
(image.language_code or "").strip().lower(): image
|
||||
for image in card.mtg_scryfall_image_ids
|
||||
}
|
||||
for language_code, image_base64 in (localized_images or {}).items():
|
||||
normalized_language_code = (language_code or "").strip().lower()
|
||||
if not normalized_language_code or not image_base64:
|
||||
continue
|
||||
|
||||
existing_image = existing_images.get(normalized_language_code)
|
||||
values = {
|
||||
"card_id": self.id,
|
||||
"language_code": normalized_language_code,
|
||||
"image_1920": image_base64,
|
||||
}
|
||||
if existing_image:
|
||||
existing_image.write({"image_1920": image_base64})
|
||||
else:
|
||||
image_model.create(values)
|
||||
|
||||
def _mtg_scryfall_sync_faces_from_payloads(self, payloads):
|
||||
"""Create or update ordered MTG card faces from localized payloads.
|
||||
|
||||
Args:
|
||||
payloads: Raw localized Scryfall payloads for one print group.
|
||||
|
||||
Returns:
|
||||
None: The card faces are synchronized in place.
|
||||
"""
|
||||
self.ensure_one()
|
||||
card = self.with_context(
|
||||
mvd_tcg_bypass_validated_write=True,
|
||||
mvd_tcg_bypass_external_ref_write=True,
|
||||
)
|
||||
|
||||
primary_payload = self._mtg_scryfall_select_primary_payload(payloads)
|
||||
primary_face_payloads = self._mtg_scryfall_extract_face_payloads(primary_payload)
|
||||
existing_faces = {face.sequence: face for face in card.mtg_face_ids}
|
||||
kept_face_ids = []
|
||||
|
||||
for index, face_payload in enumerate(primary_face_payloads, start=1):
|
||||
values = {
|
||||
"card_id": self.id,
|
||||
"sequence": index,
|
||||
"name": face_payload.get("name") or _("Face %s") % index,
|
||||
"mana_cost": face_payload.get("mana_cost") or False,
|
||||
"type_line": face_payload.get("type_line") or False,
|
||||
"oracle_text": face_payload.get("oracle_text") or False,
|
||||
"flavor_text": face_payload.get("flavor_text") or False,
|
||||
"power": face_payload.get("power") or False,
|
||||
"toughness": face_payload.get("toughness") or False,
|
||||
"loyalty": face_payload.get("loyalty") or False,
|
||||
"artist": face_payload.get("artist") or False,
|
||||
}
|
||||
face = existing_faces.get(index)
|
||||
if face:
|
||||
face.with_context(
|
||||
mvd_tcg_bypass_validated_write=True,
|
||||
mvd_tcg_bypass_external_ref_write=True,
|
||||
).write(values)
|
||||
else:
|
||||
face = self.env["mvd.tcg.mtg.card.face"].create(values)
|
||||
kept_face_ids.append(face.id)
|
||||
|
||||
(
|
||||
card.mtg_face_ids
|
||||
- self.env["mvd.tcg.mtg.card.face"].browse(kept_face_ids)
|
||||
).with_context(mvd_tcg_bypass_validated_write=True).unlink()
|
||||
|
||||
for payload in payloads or ():
|
||||
card._mtg_scryfall_update_face_translations_from_payload(payload)
|
||||
|
||||
def _mtg_scryfall_sync_legalities(self, primary_payload):
|
||||
"""Create or update legality rows from one primary Scryfall payload.
|
||||
|
||||
Args:
|
||||
primary_payload: Canonical payload for the current print group.
|
||||
|
||||
Returns:
|
||||
None: The legality rows are synchronized in place.
|
||||
"""
|
||||
self.ensure_one()
|
||||
card = self.with_context(
|
||||
mvd_tcg_bypass_validated_write=True,
|
||||
mvd_tcg_bypass_external_ref_write=True,
|
||||
)
|
||||
|
||||
legality_payload = primary_payload.get("legalities") or {}
|
||||
format_records = self._mtg_scryfall_resolve_taxonomy_records(
|
||||
"mvd.tcg.mtg.format",
|
||||
legality_payload.keys(),
|
||||
create_missing=False,
|
||||
)
|
||||
existing_legalities = {
|
||||
legality.format_id.id: legality for legality in card.mtg_legality_ids
|
||||
}
|
||||
kept_legality_ids = []
|
||||
|
||||
for format_record in format_records:
|
||||
status = (legality_payload.get(format_record.code) or "").strip().lower() or "not_legal"
|
||||
values = {
|
||||
"card_id": self.id,
|
||||
"format_id": format_record.id,
|
||||
"status": status,
|
||||
}
|
||||
legality = existing_legalities.get(format_record.id)
|
||||
if legality:
|
||||
legality.with_context(mvd_tcg_bypass_validated_write=True).write(
|
||||
{"status": status}
|
||||
)
|
||||
else:
|
||||
legality = self.env["mvd.tcg.mtg.card.legality"].create(values)
|
||||
kept_legality_ids.append(legality.id)
|
||||
|
||||
(
|
||||
card.mtg_legality_ids
|
||||
- self.env["mvd.tcg.mtg.card.legality"].browse(kept_legality_ids)
|
||||
).with_context(mvd_tcg_bypass_validated_write=True).unlink()
|
||||
|
||||
@api.depends(
|
||||
"image_1920",
|
||||
"mtg_scryfall_image_ids",
|
||||
"mtg_scryfall_image_ids.language_code",
|
||||
"mtg_scryfall_image_ids.image_1920",
|
||||
)
|
||||
@api.depends_context("lang")
|
||||
def _compute_mtg_display_images(self):
|
||||
"""Render the best localized card image for the current UI language.
|
||||
|
||||
Returns:
|
||||
None: The compute updates records in place.
|
||||
"""
|
||||
for card in self:
|
||||
localized_images = {
|
||||
(image.language_code or "").strip().lower(): image.image_1920
|
||||
for image in card.mtg_scryfall_image_ids
|
||||
if image.image_1920
|
||||
}
|
||||
display_image = False
|
||||
for language_code in card._mtg_scryfall_get_preferred_image_language_codes():
|
||||
display_image = localized_images.get(language_code)
|
||||
if display_image:
|
||||
break
|
||||
display_image = display_image or card.image_1920
|
||||
card.mtg_display_image_1920 = display_image
|
||||
card.mtg_display_image_512 = display_image
|
||||
|
||||
@api.model
|
||||
def _mtg_scryfall_prepare_group_values(self, primary_payload, *, import_run=None):
|
||||
"""Prepare ORM values for one localized Scryfall print group.
|
||||
|
||||
Args:
|
||||
primary_payload: Canonical payload that drives the shared fields.
|
||||
import_run: Optional import run record for traceability.
|
||||
|
||||
Returns:
|
||||
dict: ORM values for create or write operations.
|
||||
"""
|
||||
mtg_game = self.env["mvd.tcg.game"]._mvd_tcg_get_mtg_game()
|
||||
mtg_set = self.env["mvd.tcg.mtg.set"].mtg_scryfall_upsert_from_payload(
|
||||
primary_payload,
|
||||
import_run=import_run,
|
||||
)
|
||||
rarity = self.env["mvd.tcg.mtg.rarity"].search(
|
||||
[("code", "=", (primary_payload.get("rarity") or "").strip().lower())],
|
||||
limit=1,
|
||||
)
|
||||
color_codes = [
|
||||
(color_code or "").strip().upper()
|
||||
for color_code in (primary_payload.get("colors") or [])
|
||||
if color_code
|
||||
]
|
||||
color_records = self.env["mvd.tcg.mtg.color"].search(
|
||||
[("code", "in", color_codes)]
|
||||
)
|
||||
color_identity_codes = [
|
||||
(color_code or "").strip().upper()
|
||||
for color_code in (primary_payload.get("color_identity") or [])
|
||||
if color_code
|
||||
]
|
||||
color_identity_records = self.env["mvd.tcg.mtg.color"].search(
|
||||
[("code", "in", color_identity_codes)]
|
||||
)
|
||||
card_type_codes = self._mtg_scryfall_extract_card_type_codes(primary_payload)
|
||||
card_type_records = self.env["mvd.tcg.mtg.card.type"].search(
|
||||
[("code", "in", card_type_codes)]
|
||||
)
|
||||
keyword_display_names = {
|
||||
self._mtg_scryfall_normalize_keyword_code(keyword_name): keyword_name
|
||||
for keyword_name in (primary_payload.get("keywords") or [])
|
||||
if self._mtg_scryfall_normalize_keyword_code(keyword_name)
|
||||
}
|
||||
keyword_records = self._mtg_scryfall_resolve_taxonomy_records(
|
||||
"mvd.tcg.mtg.keyword",
|
||||
keyword_display_names.keys(),
|
||||
display_names=keyword_display_names,
|
||||
create_missing=True,
|
||||
)
|
||||
platform_records = self._mtg_scryfall_resolve_taxonomy_records(
|
||||
"mvd.tcg.mtg.platform",
|
||||
primary_payload.get("games") or [],
|
||||
)
|
||||
finish_records = self._mtg_scryfall_resolve_taxonomy_records(
|
||||
"mvd.tcg.mtg.finish",
|
||||
primary_payload.get("finishes") or [],
|
||||
)
|
||||
|
||||
values = {
|
||||
"active": True,
|
||||
"external_ref": self._mtg_scryfall_build_external_ref(primary_payload),
|
||||
"game_id": mtg_game.id,
|
||||
"state": "validated",
|
||||
"mtg_scryfall_id": primary_payload.get("id") or False,
|
||||
"mtg_scryfall_uri": primary_payload.get("scryfall_uri") or False,
|
||||
"mtg_set_id": mtg_set.id,
|
||||
"mtg_rarity_id": rarity.id or False,
|
||||
"mtg_collector_number": primary_payload.get("collector_number") or False,
|
||||
"mtg_oracle_id": primary_payload.get("oracle_id") or False,
|
||||
"mtg_layout": primary_payload.get("layout") or False,
|
||||
"mtg_mana_cost": primary_payload.get("mana_cost") or False,
|
||||
"mtg_mana_value": primary_payload.get("cmc")
|
||||
if primary_payload.get("cmc") is not None
|
||||
else False,
|
||||
"mtg_color_ids": [Command.set(color_records.ids)],
|
||||
"mtg_color_identity_ids": [Command.set(color_identity_records.ids)],
|
||||
"mtg_card_type_ids": [Command.set(card_type_records.ids)],
|
||||
"mtg_keyword_ids": [Command.set(keyword_records.ids)],
|
||||
"mtg_game_platform_ids": [Command.set(platform_records.ids)],
|
||||
"mtg_finish_ids": [Command.set(finish_records.ids)],
|
||||
"mtg_power": primary_payload.get("power") or False,
|
||||
"mtg_toughness": primary_payload.get("toughness") or False,
|
||||
"mtg_loyalty": primary_payload.get("loyalty") or False,
|
||||
"mtg_artist": primary_payload.get("artist") or False,
|
||||
"mtg_is_token": bool(
|
||||
primary_payload.get("layout") == "token"
|
||||
or primary_payload.get("set_type") == "token"
|
||||
),
|
||||
"mtg_is_reprint": bool(primary_payload.get("reprint")),
|
||||
"mtg_is_promo": bool(primary_payload.get("promo")),
|
||||
"mtg_is_digital": bool(primary_payload.get("digital")),
|
||||
"mtg_is_full_art": bool(primary_payload.get("full_art")),
|
||||
"mtg_is_textless": bool(primary_payload.get("textless")),
|
||||
}
|
||||
if import_run:
|
||||
values.update(
|
||||
{
|
||||
"mtg_scryfall_last_import_run_id": import_run.id,
|
||||
"mtg_scryfall_last_synced_at": fields.Datetime.now(),
|
||||
}
|
||||
)
|
||||
return values
|
||||
|
||||
@api.model
|
||||
def mtg_scryfall_upsert_group_from_payloads(self, payloads, import_run=None):
|
||||
"""Create or update one MTG print group from localized payloads.
|
||||
|
||||
Args:
|
||||
payloads: Raw localized Scryfall payloads for one print group.
|
||||
import_run: Optional import run record for traceability.
|
||||
|
||||
Returns:
|
||||
mvd.tcg.card: Upserted MTG card reference record.
|
||||
"""
|
||||
grouped_payloads = [
|
||||
payload
|
||||
for payload in (payloads or [])
|
||||
if payload.get("object") == "card" and payload.get("id")
|
||||
]
|
||||
if not grouped_payloads:
|
||||
return self.browse()
|
||||
|
||||
primary_payload = self._mtg_scryfall_select_primary_payload(grouped_payloads)
|
||||
external_ref = self._mtg_scryfall_build_external_ref(primary_payload)
|
||||
if not external_ref or external_ref == ":":
|
||||
return self.browse()
|
||||
|
||||
mtg_game = self.env["mvd.tcg.game"]._mvd_tcg_get_mtg_game()
|
||||
card = self.search(
|
||||
[("game_id", "=", mtg_game.id), ("external_ref", "=", external_ref)],
|
||||
limit=1,
|
||||
)
|
||||
values = self._mtg_scryfall_prepare_group_values(
|
||||
primary_payload,
|
||||
import_run=import_run,
|
||||
)
|
||||
should_write_primary_values = bool(
|
||||
not card
|
||||
or (primary_payload.get("lang") or "").strip().lower() in {"", "en"}
|
||||
)
|
||||
if should_write_primary_values:
|
||||
values.update(
|
||||
{
|
||||
"name": primary_payload.get("name")
|
||||
or primary_payload.get("printed_name")
|
||||
or external_ref.upper(),
|
||||
"mtg_type_line": primary_payload.get("type_line") or False,
|
||||
"mtg_oracle_text": primary_payload.get("oracle_text") or False,
|
||||
"mtg_flavor_text": primary_payload.get("flavor_text") or False,
|
||||
}
|
||||
)
|
||||
|
||||
localized_images = self._mtg_scryfall_collect_group_images(grouped_payloads)
|
||||
image_base64 = (
|
||||
localized_images.get((primary_payload.get("lang") or "").strip().lower())
|
||||
or localized_images.get("en")
|
||||
or self._mtg_scryfall_load_group_image_base64(grouped_payloads)
|
||||
)
|
||||
if image_base64 and (not card or not card.image_1920):
|
||||
values["image_1920"] = image_base64
|
||||
|
||||
if card:
|
||||
card = card.with_context(
|
||||
mvd_tcg_bypass_validated_write=True,
|
||||
mvd_tcg_bypass_external_ref_write=True,
|
||||
)
|
||||
card.write(values)
|
||||
else:
|
||||
if "name" not in values:
|
||||
values["name"] = (
|
||||
primary_payload.get("name")
|
||||
or primary_payload.get("printed_name")
|
||||
or external_ref.upper()
|
||||
)
|
||||
card = self.with_context(
|
||||
mvd_tcg_bypass_external_ref_write=True,
|
||||
).create(values)
|
||||
|
||||
for payload in grouped_payloads:
|
||||
card._mtg_scryfall_update_translations_from_payload(payload)
|
||||
card._mtg_scryfall_sync_faces_from_payloads(grouped_payloads)
|
||||
card._mtg_scryfall_sync_legalities(primary_payload)
|
||||
card._mtg_scryfall_sync_localized_images(localized_images)
|
||||
return card
|
||||
|
||||
@api.model
|
||||
def mtg_scryfall_upsert_from_payload(self, payload):
|
||||
"""Create or update one MTG card reference from one Scryfall payload.
|
||||
|
||||
Args:
|
||||
payload: Raw Scryfall card payload from the public API.
|
||||
|
||||
Returns:
|
||||
mvd.tcg.card: Upserted MTG card reference record.
|
||||
"""
|
||||
return self.mtg_scryfall_upsert_group_from_payloads([payload])
|
||||
|
||||
def action_open_mtg_scryfall_last_import_run(self):
|
||||
"""Open the most recent Scryfall import run linked to this card.
|
||||
|
||||
Returns:
|
||||
dict | bool: Window action or ``False`` when no run is linked.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.mtg_scryfall_last_import_run_id:
|
||||
return False
|
||||
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": self.mtg_scryfall_last_import_run_id.display_name,
|
||||
"res_model": "mvd.tcg.mtg.scryfall.import.run",
|
||||
"view_mode": "form",
|
||||
"res_id": self.mtg_scryfall_last_import_run_id.id,
|
||||
"target": "current",
|
||||
}
|
||||
|
||||
def action_mtg_scryfall_refresh_card(self):
|
||||
"""Refresh the current MTG card from the Scryfall connector.
|
||||
|
||||
Returns:
|
||||
dict: Window action for the created refresh run.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.env["mvd.tcg.mtg.scryfall.api"]._mtg_scryfall_check_manager_access()
|
||||
refresh_run = self.env["mvd.tcg.mtg.scryfall.import.run"].create_card_refresh_run(
|
||||
self
|
||||
)
|
||||
return refresh_run.action_execute_import()
|
||||
Reference in New Issue
Block a user