"""Shared utility functions for Obtainium Emulation Pack scripts.""" import copy import json import os import urllib.parse from pathlib import Path from typing import Any from urllib.parse import urlparse from constants import OBTAINIUM_SCHEME, REDIRECT_URL, SETTINGS_SCHEMA, SOURCE_HOST_MAP def load_dotenv() -> None: """Load .env into os.environ. Real env vars take precedence. Blank values skipped.""" env_path = Path(__file__).resolve().parent.parent / ".env" if not env_path.exists(): return with open(env_path, encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") key = key.strip() value = value.strip().strip("\"'") if key and value and key not in os.environ: os.environ[key] = value def detect_source_from_url(url: str) -> str | None: """Match a URL's host against SOURCE_HOST_MAP, including subdomains.""" try: host = urlparse(url).netloc.lower().lstrip("www.") except Exception: return None for domain, source in SOURCE_HOST_MAP.items(): if host == domain or host.endswith(f".{domain}"): return source return None def should_include_app(app: dict[str, Any], variant: str) -> bool: meta = app.get("meta", {}) if meta.get("excludeFromExport", False): return False if variant == "standard": return meta.get("includeInStandard", True) elif variant == "dual-screen": return meta.get("includeInDualScreen", True) return True def get_display_name(app: dict[str, Any]) -> str: return app.get("meta", {}).get("nameOverride") or app.get("name", "") def get_application_url(app: dict[str, Any]) -> str: return app.get("meta", {}).get("urlOverride") or app.get("url", "") def get_additional_settings(app: dict[str, Any]) -> dict[str, Any]: """Return additionalSettings as a dict, whether stored as object or JSON string.""" raw = app.get("additionalSettings", {}) if isinstance(raw, str): return json.loads(raw) if raw else {} if isinstance(raw, dict): return raw return {} def get_defaults_for_source(source: str | None) -> dict[str, Any]: """Return the full default settings dict for a given source type.""" defaults: dict[str, Any] = {} for key, defn in SETTINGS_SCHEMA.items(): if source is None or source in defn.sources: defaults[key] = copy.deepcopy(defn.default) return defaults def hydrate_settings(sparse: dict[str, Any], source: str | None) -> dict[str, Any]: """Merge sparse settings with schema defaults to produce a full settings dict. Keys are ordered according to SETTINGS_SCHEMA insertion order. """ defaults = get_defaults_for_source(source) defaults.update(sparse) return defaults def stringify_additional_settings( settings: dict[str, Any], source: str | None = None, ) -> str: """Hydrate and stringify settings for Obtainium consumption.""" hydrated = hydrate_settings(settings, source) return json.dumps(hydrated, separators=(",", ":")) def make_obtainium_link(app: dict[str, Any]) -> str: settings = get_additional_settings(app) source = app.get("overrideSource") payload = { "id": app["id"], "url": app["url"], "author": app["author"], "name": app["name"], "otherAssetUrls": app.get("otherAssetUrls"), "apkUrls": app.get("apkUrls"), "preferredApkIndex": app.get("preferredApkIndex"), "additionalSettings": stringify_additional_settings(settings, source), "categories": app.get("categories"), "overrideSource": app.get("overrideSource"), "allowIdChange": app.get("allowIdChange"), } encoded = urllib.parse.quote(json.dumps(payload, separators=(",", ":")), safe="") return f"{REDIRECT_URL}?r={OBTAINIUM_SCHEME}{encoded}"