Facility: 031724

Liberty Self Storage

Stale Data Warning: This facility has not been successfully scraped in 26 days (threshold: 3 days). Data may be outdated.
Facility Information active
Facility ID
031724
Name
Liberty Self Storage
URL
http://www.selfstorageliberty.com/
Address
4617 Hamilton Middletown Rd, Liberty Township, OH 45011, USA, Liberty Township, Ohio 45011
Platform
custom_facility_031724
Parser File
src/parsers/custom/facility_031724_parser.py
Last Scraped
2026-03-27 13:41:10.960303
Created
2026-03-23 02:35:08.816820
Updated
2026-03-27 13:41:10.960303
Parser & Healing Diagnosis needs_fix
Parser Status
⚠ Needs Fix
Status Reason
Parser returned 0 units
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_031724_parser.py)
"""Parser for Liberty Self Storage (Sitelink PMS SPA - units loaded dynamically)."""

from __future__ import annotations

import json
import re

from bs4 import BeautifulSoup

from src.parsers.base import BaseParser, ParseResult, UnitResult


class Facility031724Parser(BaseParser):
    """Extract storage units from Liberty Self Storage.

    This site uses Sitelink PMS with client-side JS rendering. Unit data
    is loaded dynamically via API calls. The static HTML snapshot only
    contains a 'Loading...' placeholder. We attempt to extract any unit
    data that may be embedded in script tags or JSON-LD.
    """

    platform = "custom_facility_031724"

    def parse(self, html: str, url: str = "") -> ParseResult:
        soup = BeautifulSoup(html, "lxml")
        result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)

        # Try JSON-LD structured data
        for script in soup.find_all("script", type="application/ld+json"):
            try:
                data = json.loads(script.string or "")
                self._extract_from_jsonld(data, result)
            except (json.JSONDecodeError, TypeError):
                continue

        # Try to find unit data in inline scripts
        for script in soup.find_all("script"):
            if script.string and "UnitGroup" in script.string:
                self._extract_from_script(script.string, result)

        if not result.units:
            result.warnings.append(
                "No units found - Sitelink PMS SPA loads units dynamically via API"
            )

        return result

    def _extract_from_jsonld(self, data: dict | list, result: ParseResult) -> None:
        """Extract unit data from JSON-LD Product entries."""
        if isinstance(data, list):
            for item in data:
                self._extract_from_jsonld(item, result)
            return

        if not isinstance(data, dict):
            return

        if "@graph" in data:
            for item in data["@graph"]:
                self._extract_from_jsonld(item, result)
            return

        if data.get("@type") == "Product":
            desc = data.get("description", "")
            # Pattern: "WxLxH - $PRICE - ID"
            m = re.match(r"(\d+)x(\d+)(?:x\d+)?\s*-\s*\$(\d+\.?\d*)", desc)
            if m:
                w, ln, price = float(m.group(1)), float(m.group(2)), float(m.group(3))
                unit = UnitResult()
                unit.size = f"{int(w)}x{int(ln)}"
                unit.price = price
                unit.description = data.get("category", "")
                unit.metadata = {"width": w, "length": ln, "sqft": w * ln}
                result.units.append(unit)

    def _extract_from_script(self, script_text: str, result: ParseResult) -> None:
        """Extract UnitGroup data from inline script JSON."""
        pattern = re.compile(
            r'"name":"([^"]+)","type":"([^"]+)","price":(\d+),'
            r'"__typename":"UnitGroup","amenities":\[\],"area":(\d+)'
        )
        for m in pattern.finditer(script_text):
            name, unit_type, price, _area = m.group(1), m.group(2), m.group(3), m.group(4)
            size_match = re.match(r"(\d+)x(\d+)", name)
            unit = UnitResult()
            if size_match:
                w, ln = float(size_match.group(1)), float(size_match.group(2))
                unit.size = f"{int(w)}x{int(ln)}"
                unit.metadata = {"width": w, "length": ln, "sqft": w * ln}
            unit.price = float(price)
            unit.description = unit_type
            result.units.append(unit)

Scrape Runs (3)

Run #1566 Details

Status
exported
Parser Used
Facility031724Parser
Platform Detected
unknown
Units Found
0
Stage Reached
exported
Timestamp
2026-03-27 13:41:07.359470
Timing
Stage Duration
Fetch3217ms
Detect1ms
Parse0ms
Export17ms

Snapshot: 031724_20260327T134110Z.html · Show Snapshot · Open in New Tab

No units found in this run.

All Failures for this Facility (3)

parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-27 13:41:10.942143

No units extracted for 031724

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 031724
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-27 13:41:10.636594

No units extracted for 031724

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 031724
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-23 02:40:59.427551

No units extracted for 031724

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 031724

← Back to dashboard