Facility: 112073

Volunteer Storage - Maryville 2

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
112073
Name
Volunteer Storage - Maryville 2
URL
https://ospreystorage.com/storage-locations/tennessee/maryville/volunteer-storage-maryville-2/?utm_source=google&utm_medium=organic&utm_campaign=rentstorage
Address
2924 E Lamar Alexander Pkwy, Maryville, TN 37804, USA, Maryville, Tennessee 37804
Platform
custom_facility_112073
Parser File
src/parsers/custom/facility_112073_parser.py
Last Scraped
2026-03-27 13:45:57.968094
Created
2026-03-20 23:32:48.933261
Updated
2026-03-27 13:45:58.003196
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_112073_parser.py)
"""Parser for Volunteer Storage — Maryville 2, TN.

The page uses the Osprey Storage / StorEdge "vapor-unit-table" WordPress
plugin which loads unit data via client-side API. The server-rendered HTML
contains only loading skeletons, but the page embeds a JSON-LD ``@graph``
block with a ``SelfStorage`` entity whose ``hasOfferCatalog`` lists all
unit groups as ``Product`` objects.

This facility has storage units and vehicle parking spaces. The ``name``
field encodes dimensions and category (e.g. "10x10 - Uncovered Parking",
"8x10 - Small").
"""

from __future__ import annotations

import json
import re

from bs4 import BeautifulSoup

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

_SIZE_RE = re.compile(r"(\d+(?:\.\d+)?)\s*[xX]\s*(\d+(?:\.\d+)?)")


class Facility112073Parser(BaseParser):
    """Extract storage units from Volunteer Storage Maryville 2 (JSON-LD)."""

    platform = "custom_facility_112073"

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

        products = self._find_products(soup)
        if not products:
            result.warnings.append("No SelfStorage hasOfferCatalog found in JSON-LD")
            return result

        seen: set[tuple[str, float | None]] = set()
        for product in products:
            offers = product.get("offers", {})
            raw_price = offers.get("price")

            # Skip products with no price
            if raw_price is None:
                continue

            price: float | None = None
            try:
                price = float(raw_price)
            except (TypeError, ValueError):
                price = self.normalize_price(str(raw_price))

            name = product.get("name", "")
            availability = offers.get("availability", "")

            # Parse size from name like "10x10 - Uncovered Parking"
            m = _SIZE_RE.search(name)
            if not m:
                continue

            width = float(m.group(1))
            length = float(m.group(2))
            size = f"{m.group(1)}x{m.group(2)}"

            key = (size, price)
            if key in seen:
                continue
            seen.add(key)

            # Determine category from name suffix
            category = ""
            if " - " in name:
                category = name.split(" - ", 1)[1].strip()

            is_available = "OutOfStock" not in availability

            metadata: dict = {
                "width": width,
                "length": length,
                "sqft": width * length,
            }
            if category:
                metadata["category"] = category
            if not is_available:
                metadata["available"] = False

            unit = UnitResult(
                size=size,
                price=price,
                description=name,
                url=url,
                metadata=metadata,
            )
            result.units.append(unit)

        if not result.units:
            result.warnings.append("No valid units found in JSON-LD")

        return result

    @staticmethod
    def _find_products(soup: BeautifulSoup) -> list[dict]:
        """Locate Product items from SelfStorage JSON-LD."""
        for script in soup.find_all("script", type="application/ld+json"):
            raw = script.string or ""
            if not raw.strip():
                continue
            try:
                data = json.loads(raw)
            except (json.JSONDecodeError, ValueError):
                continue

            # Check @graph for SelfStorage entity
            if isinstance(data, dict) and "@graph" in data:
                for item in data["@graph"]:
                    if not isinstance(item, dict):
                        continue
                    item_type = item.get("@type", "")
                    if isinstance(item_type, list):
                        item_type = " ".join(item_type)
                    if "SelfStorage" in str(item_type):
                        catalog = item.get("hasOfferCatalog", {})
                        products = catalog.get("itemListElement", [])
                        if products:
                            return products

            # Direct SelfStorage at top level
            if isinstance(data, dict):
                item_type = data.get("@type", "")
                if isinstance(item_type, list):
                    item_type = " ".join(item_type)
                if "SelfStorage" in str(item_type):
                    catalog = data.get("hasOfferCatalog", {})
                    products = catalog.get("itemListElement", [])
                    if products:
                        return products

        return []

Scrape Runs (4)

Run #1103 Details

Status
exported
Parser Used
Facility112073Parser
Platform Detected
storageunitsoftware
Units Found
23
Stage Reached
exported
Timestamp
2026-03-23 02:46:23.016500
Timing
Stage Duration
Fetch6710ms
Detect140ms
Parse231ms
Export7ms

Snapshot: 112073_20260323T024629Z.html · Show Snapshot · Open in New Tab

Parsed Units (23)

10x10

$64.00/mo

16x20

$102.00/mo

18x19

$109.00/mo

10x40

$84.00/mo

8x10

$147.00/mo

9x10

$162.00/mo

8x12

$164.00/mo

10x10

$155.00/mo

8x13

$169.00/mo

8x15

$174.00/mo

10x12

$184.00/mo

10x16

$217.00/mo

12x14

$230.00/mo

13x16

$112.00/mo

12x21

$149.00/mo

13x22

$120.00/mo

16x28

$192.00/mo

20x24

$199.00/mo

20x26

$199.00/mo

17x31

$182.00/mo

20x30

$224.00/mo

20x35

$302.00/mo

30x33

$294.00/mo

← Back to dashboard