119 lines
3.9 KiB
Python
119 lines
3.9 KiB
Python
"""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}"
|