mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-07 06:31:22 +02:00
83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
"""MakerWorld URL parsing and canonicalisation.
|
|
|
|
Extracts ``(model_id, profile_id_or_None)`` from model URLs and builds the
|
|
stable dedupe key used as the library ``source_url``. Rejects non-makerworld
|
|
hosts — this is the input-validation surface, so it is deliberately strict.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from urllib.parse import urlparse
|
|
|
|
from backend.app.services.model_providers.base import ProviderResourceRef
|
|
from backend.app.services.model_providers.makerworld.errors import MakerWorldUrlError
|
|
|
|
MAKERWORLD_HOST = "makerworld.com" # Used only for URL parsing (input validation)
|
|
|
|
_MODEL_ID_RE = re.compile(r"/models/(\d+)")
|
|
_PROFILE_ID_RE = re.compile(r"#profileId[-=](\d+)")
|
|
|
|
|
|
def parse_url(url: str) -> ProviderResourceRef:
|
|
"""Extract a :class:`ProviderResourceRef` from a MakerWorld URL.
|
|
|
|
Accepts any of:
|
|
- ``https://makerworld.com/en/models/1400373``
|
|
- ``https://makerworld.com/en/models/1400373-slug-with-dashes``
|
|
- ``https://makerworld.com/en/models/1400373#profileId-1452154``
|
|
- ``makerworld.com/models/1400373`` (scheme optional)
|
|
|
|
Rejects non-makerworld hosts.
|
|
"""
|
|
if not url or not isinstance(url, str):
|
|
raise MakerWorldUrlError("URL is empty or not a string")
|
|
candidate = url.strip()
|
|
if "://" not in candidate:
|
|
candidate = "https://" + candidate
|
|
try:
|
|
parsed = urlparse(candidate)
|
|
except ValueError as exc:
|
|
raise MakerWorldUrlError(f"Could not parse URL: {exc}") from exc
|
|
|
|
host = (parsed.hostname or "").lower()
|
|
if host != MAKERWORLD_HOST and not host.endswith("." + MAKERWORLD_HOST):
|
|
raise MakerWorldUrlError(f"Not a MakerWorld URL (host={host!r}); expected makerworld.com")
|
|
|
|
model_match = _MODEL_ID_RE.search(parsed.path)
|
|
if not model_match:
|
|
raise MakerWorldUrlError("URL does not contain a /models/{id} segment")
|
|
model_id = int(model_match.group(1))
|
|
|
|
profile_id: int | None = None
|
|
if parsed.fragment:
|
|
profile_match = _PROFILE_ID_RE.search("#" + parsed.fragment)
|
|
if profile_match:
|
|
profile_id = int(profile_match.group(1))
|
|
|
|
return ProviderResourceRef(
|
|
source_type="makerworld",
|
|
external_id=str(model_id),
|
|
sub_id=str(profile_id) if profile_id is not None else None,
|
|
original_url=url,
|
|
)
|
|
|
|
|
|
def canonical_url(ref: ProviderResourceRef) -> str:
|
|
"""Build a stable dedupe key for a MakerWorld resource.
|
|
|
|
Dedupe is keyed per *plate* (profile) rather than per model, since the
|
|
download returns a specific plate — not the full multi-plate zip — so two
|
|
different plates of the same design should become two separate library
|
|
entries. Canonical shape uses the locale-free path with the
|
|
``#profileId-`` fragment so all URL variants of the same plate still
|
|
collapse (e.g. ``/en/models/123-slug?from=search#profileId-456`` and
|
|
``/de/models/123#profileId-456`` both map to
|
|
``https://makerworld.com/models/123#profileId-456``). Plate-less imports
|
|
(legacy or whole-design) keep the old model-only shape for backwards
|
|
compatibility with existing rows.
|
|
"""
|
|
if ref.sub_id:
|
|
return f"https://makerworld.com/models/{ref.external_id}#profileId-{ref.sub_id}"
|
|
return f"https://makerworld.com/models/{ref.external_id}"
|