Facility: 003628

Affordable Mini 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
003628
Name
Affordable Mini Storage
URL
http://www.affordableministoragemtvernon.com/storage-units
Address
112 Pittsburgh Ave, MT Vernon, OH 43050, USA, Mt Vernon, Ohio 43050
Platform
custom_facility_003628
Parser File
src/parsers/custom/facility_003628_parser.py
Last Scraped
2026-03-27 13:56:42.138265
Created
2026-03-14 16:21:53.706708
Updated
2026-03-27 13:56:42.165425
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_003628_parser.py)
"""Parser for Affordable Mini Storage (Hibu/DudaMobile restaurant-menu widget)."""

from __future__ import annotations

import base64
import json
import re

from bs4 import BeautifulSoup

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


class Facility003628Parser(BaseParser):
    """Extract storage units from Affordable Mini Storage.

    The site uses a Hibu "restaurant menu" widget that stores unit data in a
    base64-encoded JSON blob in the ``menu_data`` attribute.  As a fallback the
    parser also scrapes the rendered ``.menuItemBox`` DOM elements.

    Note: This facility does not publish dollar prices on its website.  Units
    are extracted with sizes only; the price field will be ``None`` for most
    entries.
    """

    platform = "custom_facility_003628"

    # Matches dimension strings like 5' x 10', 10'x20', etc.
    _SIZE_RE = re.compile(
        r"(\d+)\s*['\u2032]?\s*[xX\u00d7]\s*(\d+)\s*['\u2032]?"
    )

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

        # --- Strategy 1: Parse the base64 menu_data JSON blob ---
        units_from_json = self._parse_menu_data_json(soup)
        if units_from_json:
            result.units = units_from_json
        else:
            # --- Strategy 2: Parse rendered DOM elements ---
            result.units = self._parse_menu_dom(soup)

        if not result.units:
            result.warnings.append("No units found on page")

        return result

    # ------------------------------------------------------------------
    # Strategy 1 – base64 JSON blob
    # ------------------------------------------------------------------
    def _parse_menu_data_json(self, soup: BeautifulSoup) -> list[UnitResult]:
        units: list[UnitResult] = []
        menu_el = soup.find(attrs={"menu_data": True})
        if not menu_el:
            return units

        raw = menu_el.get("menu_data", "")
        try:
            decoded = base64.b64decode(raw)
            sections = json.loads(decoded)
        except Exception:
            return units

        currency = menu_el.get("currency", "$")

        for section in sections:
            for entry in section.get("entries", []):
                title = (entry.get("title") or "").strip()
                if not title:
                    continue

                unit = UnitResult()

                # Try to extract a structured size
                m = self._SIZE_RE.search(title)
                if m:
                    unit.size = f"{m.group(1)}' x {m.group(2)}'"
                    w, ln, sq = self.normalize_size(unit.size)
                    if w is not None:
                        unit.metadata = {"width": w, "length": ln, "sqft": sq}
                else:
                    # Non-dimensional entry (e.g. "Outside camper and boat storage")
                    unit.size = title

                # Extract price if present
                prices = entry.get("prices", [])
                if prices:
                    price_str = (prices[0].get("price") or "").strip()
                    if price_str and price_str.replace(currency, "").strip():
                        parsed_price = self.normalize_price(price_str)
                        if parsed_price is not None:
                            unit.price = parsed_price
                        else:
                            # Non-numeric price like "Please call"
                            unit.description = price_str

                desc = (entry.get("desc") or "").strip()
                if desc and not unit.description:
                    unit.description = desc

                units.append(unit)

        return units

    # ------------------------------------------------------------------
    # Strategy 2 – rendered DOM
    # ------------------------------------------------------------------
    def _parse_menu_dom(self, soup: BeautifulSoup) -> list[UnitResult]:
        units: list[UnitResult] = []
        for box in soup.select(".menuItemBox"):
            name_el = box.select_one(".menuItemName")
            price_el = box.select_one(".menuItemPrice")

            name = name_el.get_text(strip=True) if name_el else ""
            if not name:
                continue

            unit = UnitResult()

            m = self._SIZE_RE.search(name)
            if m:
                unit.size = f"{m.group(1)}' x {m.group(2)}'"
                w, ln, sq = self.normalize_size(unit.size)
                if w is not None:
                    unit.metadata = {"width": w, "length": ln, "sqft": sq}
            else:
                unit.size = name

            if price_el:
                price_text = price_el.get_text(strip=True)
                if price_text:
                    parsed_price = self.normalize_price(price_text)
                    if parsed_price is not None:
                        unit.price = parsed_price
                    else:
                        unit.description = price_text

            units.append(unit)

        return units

Scrape Runs (5)

Run #1233 Details

Status
exported
Parser Used
Facility003628Parser
Platform Detected
table_layout
Units Found
4
Stage Reached
exported
Timestamp
2026-03-23 02:57:57.740022
Timing
Stage Duration
Fetch6317ms
Detect37ms
Parse20ms
Export6ms

Snapshot: 003628_20260323T025804Z.html · Show Snapshot · Open in New Tab

Parsed Units (4)

5' x 10'

No price

10' x 10'

No price

10' x 20'

No price

Outside camper and boat storage

No price

← Back to dashboard