add apk filters, about descriptions, version extraction regex to app configs. fix Cemu versioning. refactor: store additionalSettings as real JSON in source file, stringify at export boundaries in scripts

This commit is contained in:
Richard Macias
2026-02-28 00:04:34 -06:00
parent 9c9cbe0bc8
commit 5a0ff4f4de
11 changed files with 1756 additions and 185 deletions

View File

@@ -193,7 +193,7 @@ def generate_app_entry(
"author": author,
"name": name,
"preferredApkIndex": 0,
"additionalSettings": json.dumps(settings, separators=(",", ":")),
"additionalSettings": settings,
"categories": categories,
"allowIdChange": allow_id_change,
"overrideSource": source,

View File

@@ -5,7 +5,7 @@ import json
import sys
from typing import Any
from utils import should_include_app
from utils import should_include_app, stringify_additional_settings
def minify_json(input_file: str, output_file: str, variant: str = "standard") -> None:
@@ -19,6 +19,7 @@ def minify_json(input_file: str, output_file: str, variant: str = "standard") ->
if should_include_app(app, variant):
app_copy = app.copy()
app_copy.pop("meta", None)
app_copy["additionalSettings"] = stringify_additional_settings(app_copy)
filtered_apps.append(app_copy)
data["apps"] = filtered_apps

View File

@@ -3,6 +3,7 @@
import json
import sys
from pathlib import Path
from typing import Any
from constants import SRC_FILE
@@ -20,28 +21,83 @@ KEY_ORDER = [
"meta",
]
# Canonical key order for additionalSettings - source-specific keys first,
# then common keys, grouped logically. Matches DEFAULT_ADDITIONAL_SETTINGS
# in add-app.py with source-specific keys prepended.
SETTINGS_KEY_ORDER = [
# GitHub/Codeberg source-specific
"includePrereleases",
"fallbackToOlderReleases",
"filterReleaseTitlesByRegEx",
"filterReleaseNotesByRegEx",
"verifyLatestTag",
"sortMethodChoice",
"useLatestAssetDateAsReleaseDate",
"releaseTitleAsVersion",
"github-creds",
"GHReqPrefix",
# HTML source-specific
"intermediateLink",
"customLinkFilterRegex",
"filterByLinkText",
"matchLinksOutsideATags",
"skipSort",
"reverseSort",
"sortByLastLinkSegment",
"versionExtractWholePage",
"requestHeader",
"defaultPseudoVersioningMethod",
# Common keys
"trackOnly",
"versionExtractionRegEx",
"matchGroupToUse",
"versionDetection",
"releaseDateAsVersion",
"useVersionCodeAsOSVersion",
"apkFilterRegEx",
"invertAPKFilter",
"autoApkFilterByArch",
"appName",
"appAuthor",
"shizukuPretendToBeGooglePlay",
"allowInsecure",
"exemptFromBackgroundUpdates",
"skipUpdateNotifications",
"about",
"refreshBeforeDownload",
"includeZips",
"zippedApkFilterRegEx",
]
# Fields to backfill with defaults when missing
DEFAULTS: dict[str, object] = {
"allowIdChange": False,
}
def _order_dict(d: dict[str, Any], key_order: list[str]) -> dict[str, Any]:
ordered: dict[str, Any] = {}
for key in key_order:
if key in d:
ordered[key] = d[key]
# Preserve any unexpected keys at the end (safety net)
for key in d:
if key not in ordered:
ordered[key] = d[key]
return ordered
def normalize_app(app: dict) -> dict:
for key, default in DEFAULTS.items():
if key not in app:
app[key] = default
ordered: dict[str, object] = {}
for key in KEY_ORDER:
if key in app:
ordered[key] = app[key]
# Normalize additionalSettings key order if it's a dict
settings = app.get("additionalSettings")
if isinstance(settings, dict):
app["additionalSettings"] = _order_dict(settings, SETTINGS_KEY_ORDER)
# Preserve any unexpected keys at the end (safety net)
for key in app:
if key not in ordered:
ordered[key] = app[key]
return ordered
return _order_dict(app, KEY_ORDER)
def normalize(input_path: str) -> int:

View File

@@ -18,7 +18,7 @@ from urllib.parse import urljoin, urlparse
from urllib.request import Request, urlopen
from help_formatter import StyledHelpFormatter
from utils import load_dotenv
from utils import get_additional_settings, load_dotenv
USER_AGENT = (
"Mozilla/5.0 (Linux; Android 10; K) "
@@ -495,12 +495,11 @@ def _effective_source(app: dict[str, Any]) -> str:
def test_app(app: dict[str, Any]) -> TestResult:
source = _effective_source(app)
settings_str = app.get("additionalSettings", "{}")
try:
settings = json.loads(settings_str) if isinstance(settings_str, str) else {}
except json.JSONDecodeError:
settings = get_additional_settings(app)
except (json.JSONDecodeError, TypeError):
result = TestResult(app.get("name", "?"), app.get("id", "?"), source, app.get("url", "?"))
result.error = "Cannot parse additionalSettings JSON"
result.error = "Cannot parse additionalSettings"
return result
start = time.monotonic()

View File

@@ -46,6 +46,24 @@ 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 stringify_additional_settings(app: dict[str, Any]) -> str:
"""Return additionalSettings as a compact JSON string for Obtainium consumption."""
raw = app.get("additionalSettings", {})
if isinstance(raw, str):
return raw
return json.dumps(raw, separators=(",", ":"))
def make_obtainium_link(app: dict[str, Any]) -> str:
payload = {
"id": app["id"],
@@ -55,7 +73,7 @@ def make_obtainium_link(app: dict[str, Any]) -> str:
"otherAssetUrls": app.get("otherAssetUrls"),
"apkUrls": app.get("apkUrls"),
"preferredApkIndex": app.get("preferredApkIndex"),
"additionalSettings": app.get("additionalSettings"),
"additionalSettings": stringify_additional_settings(app),
"categories": app.get("categories"),
"overrideSource": app.get("overrideSource"),
"allowIdChange": app.get("allowIdChange"),

View File

@@ -15,7 +15,7 @@ from constants import (
VALID_SOURCES,
VARIANTS,
)
from utils import should_include_app
from utils import get_additional_settings, should_include_app
REQUIRED_FIELDS = {"id", "url", "author", "name"}
@@ -155,14 +155,10 @@ def _validate_additional_settings(
if raw is None:
return errors, warnings
if not isinstance(raw, str):
errors.append(f"{app_name}: 'additionalSettings' should be a JSON string")
return errors, warnings
try:
settings = json.loads(raw)
except json.JSONDecodeError as e:
errors.append(f"{app_name}: 'additionalSettings' contains invalid JSON: {e}")
settings = get_additional_settings(app)
except (json.JSONDecodeError, TypeError) as e:
errors.append(f"{app_name}: 'additionalSettings' is invalid: {e}")
return errors, warnings
if not isinstance(settings, dict):