spring cleaning and refactoring

This commit is contained in:
Richard Macias
2026-02-14 15:02:53 -06:00
parent 5c752624af
commit 3851aff4c3
21 changed files with 1159 additions and 595 deletions

View File

@@ -7,6 +7,8 @@ import sys
from pathlib import Path
from urllib.parse import urlparse
from utils import load_dotenv
# Default Obtainium settings for GitHub apps
DEFAULT_ADDITIONAL_SETTINGS = {
"includePrereleases": False,
@@ -46,10 +48,10 @@ CATEGORIES = [
]
VARIANT_OPTIONS = [
"Both", # Include in both standard and dual-screen
"Standard only", # Include in standard only
"Dual-screen only", # Include in dual-screen only
"README only", # Exclude from export, show in table only
"Both",
"Standard only",
"Dual-screen only",
"README only",
]
SOURCE_DETECTION = {
@@ -61,7 +63,6 @@ SOURCE_DETECTION = {
def detect_source(url: str):
"""Detect the source type from URL."""
parsed = urlparse(url)
host = parsed.netloc.lower()
for domain, source in SOURCE_DETECTION.items():
@@ -71,7 +72,6 @@ def detect_source(url: str):
def extract_github_info(url: str) -> tuple[str, str] | None:
"""Extract author and repo name from GitHub URL."""
match = re.match(r"https?://github\.com/([^/]+)/([^/]+)", url)
if match:
return match.group(1), match.group(2)
@@ -79,7 +79,6 @@ def extract_github_info(url: str) -> tuple[str, str] | None:
def prompt(message: str, default: str = "") -> str:
"""Prompt user for input with optional default."""
if default:
result = input(f"{message} [{default}]: ").strip()
return result if result else default
@@ -87,7 +86,6 @@ def prompt(message: str, default: str = "") -> str:
def prompt_yes_no(message: str, default: bool = True) -> bool:
"""Prompt user for yes/no."""
default_str = "Y/n" if default else "y/N"
result = input(f"{message} [{default_str}]: ").strip().lower()
if not result:
@@ -96,7 +94,6 @@ def prompt_yes_no(message: str, default: bool = True) -> bool:
def select_menu(title: str, choices: list[str], default: int = 0) -> str:
"""Interactive menu with arrow key navigation."""
# Only use curses if we have a real terminal
if not sys.stdin.isatty():
return _select_menu_fallback(title, choices, default)
@@ -111,10 +108,9 @@ def select_menu(title: str, choices: list[str], default: int = 0) -> str:
def _select_menu_curses(title: str, choices: list[str], default: int = 0) -> str:
"""Curses-based interactive menu."""
def menu(stdscr):
curses.curs_set(0) # Hide cursor
curses.curs_set(0)
current = default
while True:
@@ -140,7 +136,7 @@ def _select_menu_curses(title: str, choices: list[str], default: int = 0) -> str
current += 1
elif key in (curses.KEY_ENTER, 10, 13):
return choices[current]
elif key == 27: # ESC
elif key == 27:
return choices[default]
import curses
@@ -149,7 +145,6 @@ def _select_menu_curses(title: str, choices: list[str], default: int = 0) -> str
def _select_menu_fallback(title: str, choices: list[str], default: int = 0) -> str:
"""Fallback menu using simple input."""
print(f"\n{title}")
for i, choice in enumerate(choices):
marker = ">" if i == default else " "
@@ -182,7 +177,6 @@ def generate_app_entry(
app_name_override: str | None = None,
url_override: str | None = None,
) -> dict:
"""Generate a complete app entry."""
settings = DEFAULT_ADDITIONAL_SETTINGS.copy()
if "Track Only" in categories:
settings["trackOnly"] = True
@@ -205,7 +199,6 @@ def generate_app_entry(
"overrideSource": source,
}
# Build meta based on variant selection
meta = {}
if variant == "Standard only":
meta["includeInDualScreen"] = False
@@ -213,7 +206,6 @@ def generate_app_entry(
meta["includeInStandard"] = False
elif variant == "README only":
meta["excludeFromExport"] = True
# "Both" = no meta needed (default behavior)
if app_name_override:
meta["nameOverride"] = app_name_override
@@ -227,7 +219,6 @@ def generate_app_entry(
def main() -> int:
"""Interactive CLI to add a new app."""
print("=" * 50)
print(" Add New App to Obtainium Emulation Pack")
print("=" * 50)
@@ -349,7 +340,23 @@ def main() -> int:
f.write("\n")
print(f"\nApp added to {apps_file}")
print("\nNext steps:")
# Offer to live-test the new app config
if prompt_yes_no("\nRun live test on this app?", True):
load_dotenv()
# Import here to avoid circular deps and keep startup fast
from importlib import import_module
test_mod = import_module("test-apps")
print()
result = test_mod.test_app(app_entry)
test_mod.print_result(result, verbose=True)
if not result.passed:
print("\nThe app config failed the live test.")
print("The entry has been saved - you may want to fix the config and re-test.")
print()
print("Next steps:")
print(" 1. Run 'make release' to regenerate all files")
print(" 2. Review the diff before committing")

View File

@@ -1,13 +1,168 @@
"""Shared constants for Obtainium Emulation Pack scripts."""
# File paths
SRC_FILE = "src/applications.json"
PAGES_DIR = "pages"
TABLE_FILE = "pages/table.md"
# Obtainium settings
REDIRECT_URL = "http://apps.obtainium.imranr.dev/redirect.html"
OBTAINIUM_SCHEME = "obtainium://app/"
# Supported variants
VARIANTS = ("standard", "dual-screen")
# ---------------------------------------------------------------------------
# Obtainium source types and settings schema
# Derived from Obtainium source code: lib/app_sources/*.dart
# ---------------------------------------------------------------------------
# Valid overrideSource values (runtime type names from Obtainium)
VALID_SOURCES = {
"GitHub",
"GitLab",
"Codeberg",
"FDroid",
"FDroidRepo",
"IzzyOnDroid",
"SourceHut",
"APKPure",
"Aptoide",
"Uptodown",
"HuaweiAppGallery",
"Tencent",
"VivoAppStore",
"RuStore",
"Farsroid",
"CoolApk",
"RockMods",
"LiteAPKs",
"Jenkins",
"APKMirror",
"TelegramApp",
"NeutronCode",
"SourceForge",
"DirectAPKLink",
"HTML",
}
# URL host-to-source mapping for auto-detection
SOURCE_HOST_MAP = {
"github.com": "GitHub",
"gitlab.com": "GitLab",
"codeberg.org": "Codeberg",
"f-droid.org": "FDroid",
"android.izzysoft.de": "IzzyOnDroid",
"git.sr.ht": "SourceHut",
"apkpure.net": "APKPure",
"aptoide.com": "Aptoide",
"uptodown.com": "Uptodown",
"appgallery.huawei.com": "HuaweiAppGallery",
"sj.qq.com": "Tencent",
"h5.appstore.vivo.com.cn": "VivoAppStore",
"rustore.ru": "RuStore",
"farsroid.com": "Farsroid",
"coolapk.com": "CoolApk",
"rockmods.net": "RockMods",
"liteapks.com": "LiteAPKs",
"apkmirror.com": "APKMirror",
"telegram.org": "TelegramApp",
"neutroncode.com": "NeutronCode",
"sourceforge.net": "SourceForge",
}
# Common additionalSettings keys valid for all source types
COMMON_SETTINGS_KEYS = {
"trackOnly",
"versionExtractionRegEx",
"matchGroupToUse",
"versionDetection",
"useVersionCodeAsOSVersion",
"apkFilterRegEx",
"invertAPKFilter",
"autoApkFilterByArch",
"appName",
"appAuthor",
"shizukuPretendToBeGooglePlay",
"allowInsecure",
"exemptFromBackgroundUpdates",
"skipUpdateNotifications",
"about",
"refreshBeforeDownload",
"releaseDateAsVersion",
"includeZips",
"zippedApkFilterRegEx",
}
# Source-specific additionalSettings keys
SOURCE_SPECIFIC_KEYS: dict[str, set[str]] = {
"GitHub": {
"includePrereleases",
"fallbackToOlderReleases",
"filterReleaseTitlesByRegEx",
"filterReleaseNotesByRegEx",
"verifyLatestTag",
"sortMethodChoice",
"useLatestAssetDateAsReleaseDate",
"releaseTitleAsVersion",
"dontSortReleasesList",
"github-creds",
},
"GitLab": {
"fallbackToOlderReleases",
},
"Codeberg": {
# Inherits GitHub's settings
"includePrereleases",
"fallbackToOlderReleases",
"filterReleaseTitlesByRegEx",
"filterReleaseNotesByRegEx",
"verifyLatestTag",
"sortMethodChoice",
"useLatestAssetDateAsReleaseDate",
"releaseTitleAsVersion",
"dontSortReleasesList",
},
"FDroid": {
"filterVersionsByRegEx",
"trySelectingSuggestedVersionCode",
"autoSelectHighestVersionCode",
},
"FDroidRepo": {
"appIdOrName",
"pickHighestVersionCode",
"trySelectingSuggestedVersionCode",
},
"HTML": {
"intermediateLink",
"customLinkFilterRegex",
"filterByLinkText",
"matchLinksOutsideATags",
"skipSort",
"reverseSort",
"sortByLastLinkSegment",
"versionExtractWholePage",
"requestHeader",
"defaultPseudoVersioningMethod",
"supportFixedAPKURL",
"sortByFileNamesNotLinks",
},
"DirectAPKLink": {
"intermediateLink",
"customLinkFilterRegex",
"filterByLinkText",
"skipSort",
"reverseSort",
"sortByLastLinkSegment",
"requestHeader",
"defaultPseudoVersioningMethod",
},
}
# Settings keys that contain regex patterns (should be validated)
REGEX_SETTINGS_KEYS = {
"apkFilterRegEx",
"versionExtractionRegEx",
"filterReleaseTitlesByRegEx",
"filterReleaseNotesByRegEx",
"customLinkFilterRegex",
"filterVersionsByRegEx",
"zippedApkFilterRegEx",
}

View File

@@ -1,49 +0,0 @@
"""Generate click-to-install Obtainium URLs for all apps."""
import json
import sys
import urllib.parse
from typing import Any
from constants import OBTAINIUM_SCHEME, REDIRECT_URL
def generate_obtainium_url(app: dict[str, Any]) -> str:
"""Generate an Obtainium deep-link URL for an app."""
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": app.get("additionalSettings"),
"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}"
def main(json_file: str) -> None:
"""Print Obtainium URLs for all apps in the JSON file."""
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
if "apps" not in data:
print("Invalid JSON format: Missing 'apps' key.")
sys.exit(1)
for app in data["apps"]:
obtainium_url = generate_obtainium_url(app)
print(f"{app['name']}: {obtainium_url}\n")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python generate_obtainium_urls.py <json_file>")
sys.exit(1)
main(sys.argv[1])

View File

@@ -5,12 +5,6 @@ from pathlib import Path
def stitch_markdown_files(markdown_files: list[str], output_file: str = "README.md") -> None:
"""Concatenate markdown files with double newlines between sections.
Args:
markdown_files: List of paths to markdown files to combine
output_file: Path to write the combined output
"""
combined_content = []
for file in markdown_files:

View File

@@ -2,36 +2,13 @@
import json
import sys
import urllib.parse
from collections import defaultdict
from typing import Any
from constants import OBTAINIUM_SCHEME, REDIRECT_URL
from utils import get_application_url, get_display_name, should_include_app
def make_obtainium_link(app: dict[str, Any]) -> str:
"""Generate an Obtainium deep-link URL for an app."""
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": app.get("additionalSettings"),
"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}"
from utils import get_application_url, get_display_name, make_obtainium_link, should_include_app
def generate_category_tables(apps: list[dict[str, Any]]) -> str:
"""Generate markdown tables grouped by category."""
# Categorize apps
categorized: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
for app in apps:
categories = app.get("categories", [])
@@ -78,7 +55,6 @@ def generate_category_tables(apps: list[dict[str, Any]]) -> str:
def main(input_file: str, output_file: str) -> None:
"""Generate category-based markdown table from applications.json."""
with open(input_file, "r", encoding="utf-8") as f:
data = json.load(f)

View File

@@ -1,84 +0,0 @@
aPS3e: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22aenu.aps3e%22%2C%22url%22%3A%22https%3A//github.com/aenu1/aps3e%22%2C%22author%22%3A%22aenu1%22%2C%22name%22%3A%22aPS3e%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%7D
Azahar: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22io.github.lime3ds.android%22%2C%22url%22%3A%22https%3A//github.com/azahar-emu/azahar%22%2C%22author%22%3A%22azahar-emu%22%2C%22name%22%3A%22Azahar%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
Cemu: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22info.cemu.cemu%22%2C%22url%22%3A%22https%3A//github.com/SSimco/Cemu%22%2C%22author%22%3A%22SSimco%22%2C%22name%22%3A%22Cemu%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22meta%22%3A%7B%22includeInDualScreen%22%3Afalse%7D%7D
Citra: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.citra.emu%22%2C%22url%22%3A%22https%3A//github.com/weihuoya/citra%22%2C%22author%22%3A%22weihuoya%22%2C%22name%22%3A%22Citra%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Citra%20MMJ%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%2C%22meta%22%3A%7B%22nameOverride%22%3A%22Citra%20MMJ%22%7D%7D
Citron: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.citron.citron_emu%22%2C%22url%22%3A%22https%3A//git.citron-emu.org/Citron/Emulator%22%2C%22author%22%3A%22citron%22%2C%22name%22%3A%22Citron%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22app-mainline-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22Codeberg%22%2C%22allowIdChange%22%3Afalse%7D
Citron Nightly: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.citron.citron_emu%22%2C%22url%22%3A%22https%3A//github.com/Zephyron-Dev/Citron-CI%22%2C%22author%22%3A%22Zephyron-Dev%22%2C%22name%22%3A%22Citron%20Nightly%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22app-mainline-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%2C%22meta%22%3A%7B%22excludeFromExport%22%3Atrue%7D%7D
Dolphin Emulator (Dev build): http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.dolphinemu.dolphinemu%22%2C%22url%22%3A%22https%3A//dolphin-emu.org/download/%3Fref%3Dbtn%22%2C%22author%22%3A%22dolphin-emu.org%22%2C%22name%22%3A%22Dolphin%20Emulator%20%28Dev%20build%29%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22https%3A%5C%5C%5C%5C/%5C%5C%5C%5C/dl.dolphin-emu.org%5C%5C%5C%5C/builds.%2Bdolphin-master-.%2B.apk%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Atrue%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla/5.0%20%28Linux%3B%20Android%2010%3B%20K%29%20AppleWebKit/537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome/114.0.0.0%20Mobile%20Safari/537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22partialAPKHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22dolphin-master-%28.%2B%29.apk%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%241%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Dolphin%20Emulator%20%28Dev%20build%29%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22HTML%22%2C%22meta%22%3A%7B%22urlOverride%22%3A%22https%3A//dolphin-emu.org%22%2C%22excludeFromExport%22%3Atrue%7D%7D
Dolphin-MMJR2-VBI: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.dolphinemu.mmjr%22%2C%22url%22%3A%22https%3A//github.com/Medard22/Dolphin-MMJR2-VBI%22%2C%22author%22%3A%22Medard22%22%2C%22name%22%3A%22Dolphin-MMJR2-VBI%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22meta%22%3A%7B%22excludeFromExport%22%3Atrue%7D%7D
Dolphin Emulator: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.dolphinemu.dolphinemu%22%2C%22url%22%3A%22https%3A//dolphin-emu.org/download/%3Fref%3Dbtn%22%2C%22author%22%3A%22dolphin-emu.org%22%2C%22name%22%3A%22Dolphin%20Emulator%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22https%3A%5C%5C%5C%5C/%5C%5C%5C%5C/dl.dolphin-emu.org%5C%5C%5C%5C/releases.%2Bdolphin.%2B.apk%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Atrue%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla/5.0%20%28Linux%3B%20Android%2010%3B%20K%29%20AppleWebKit/537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome/114.0.0.0%20Mobile%20Safari/537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22partialAPKHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22/releases/.%2B/dolphin-%28.%2B%29.apk%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%241%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Dolphin%20Emulator%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22HTML%22%2C%22meta%22%3A%7B%22urlOverride%22%3A%22https%3A//dolphin-emu.org%22%7D%7D
DuckStation: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.github.stenzek.duckstation%22%2C%22url%22%3A%22https%3A//duckstation-mirror.rmacias.workers.dev%22%2C%22author%22%3A%22duckstation-mirror.rmacias.workers.dev%22%2C%22name%22%3A%22DuckStation%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22matchLinksOutsideATags%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Atrue%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla/5.0%20%28Linux%3B%20Android%2010%3B%20K%29%20AppleWebKit/537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome/114.0.0.0%20Mobile%20Safari/537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22partialAPKHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22Version%3A.%2A%3F%28%5B%5C%5C%5C%5Cd.-%5D%2B%29%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%221%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Duckstation%20%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22stenzek%20%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22HTML%22%2C%22allowIdChange%22%3Afalse%7D
Releases: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22dev.eden.eden_emulator%22%2C%22url%22%3A%22https%3A//github.com/eden-emulator/Releases%22%2C%22author%22%3A%22eden-emulator%22%2C%22name%22%3A%22Releases%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Eden%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22meta%22%3A%7B%22nameOverride%22%3A%22Eden%22%2C%22urlOverride%22%3A%22https%3A//eden-emulator.github.io%22%7D%7D
Eden Nightly: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22dev.eden.eden_nightly%22%2C%22url%22%3A%22https%3A//github.com/pflyly/eden-nightly%22%2C%22author%22%3A%22pflyly%22%2C%22name%22%3A%22Eden%20Nightly%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%2C%22meta%22%3A%7B%22excludeFromExport%22%3Atrue%7D%7D
Flycast: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.flycast.emulator%22%2C%22url%22%3A%22https%3A//github.com/flyinghead/flycast%22%2C%22author%22%3A%22flyinghead%22%2C%22name%22%3A%22Flycast%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
MelonDS: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22me.magnum.melonds%22%2C%22url%22%3A%22https%3A//github.com/rafaelvcaetano/melonDS-android%22%2C%22author%22%3A%22rafaelvcaetano%22%2C%22name%22%3A%22MelonDS%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22MelonDS%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22github-creds%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22meta%22%3A%7B%22includeInStandard%22%3Atrue%2C%22includeInDualScreen%22%3Afalse%7D%7D
MelonDS Nightly: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22me.magnum.melonds.nightly%22%2C%22url%22%3A%22https%3A//github.com/rafaelvcaetano/melonDS-android%22%2C%22author%22%3A%22rafaelvcaetano%22%2C%22name%22%3A%22MelonDS%20Nightly%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22fallbackToOlderReleases%5C%22%3Afalse%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Atrue%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22.%2Anightly.%2A%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22MelonDS%20%28nightly%29%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22meta%22%3A%7B%22excludeFromExport%22%3Atrue%7D%7D
Kenji-NX: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.kenjinx.android%22%2C%22url%22%3A%22https%3A//GitHub.com/Kenji-NX/Android-Releases%22%2C%22author%22%3A%22Kenji-NX%22%2C%22name%22%3A%22Kenji-NX%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22meta%22%3A%7B%22urlOverride%22%3A%22https%3A//git.ryujinx.app/kenji-nx/ryujinx/-/tree/libryujinx_bionic%22%2C%22excludeFromExport%22%3Atrue%7D%7D
PPSSPP: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.ppsspp.ppsspp%22%2C%22url%22%3A%22https%3A//www.ppsspp.org/download%22%2C%22author%22%3A%22www.ppsspp.org%22%2C%22name%22%3A%22PPSSPP%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%5C%5C%5C/%5B0-9%5D%2B%28%3F%3A_%5B0-9%5D%2B%29%7B1%2C2%7D%5C%5C%5C%5C/ppsspp%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla/5.0%20%28Linux%3B%20Android%2010%3B%20K%29%20AppleWebKit/537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome/114.0.0.0%20Mobile%20Safari/537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22APKLinkHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%5C%5C%5C/%28%3F%3A%28%3F%3A%28%5B0-9%5D%2B%29_%28%5B0-9%5D%2B%29%5C%5C%5C%5C/ppsspp%5C%5C%5C%5C.%29%7C%28%3F%3A%28%5B0-9%5D%2B%29_%28%5B0-9%5D%2B%29_%28%5B0-9%5D%2B%29%5C%5C%5C%5C/ppsspp%28%5C%5C%5C%5C.%29%29%29apk%24%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%241%243.%242%244%246%245%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22PPSSPP%5C%22%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22PPSSPP%20is%20an%20open%20source%2C%20fast%20and%20portable%20PSP%20emulator%5C%22%2C%5C%22supportFixedAPKURL%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22HTML%22%7D
RetroArch (AArch64): http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22487343354%22%2C%22url%22%3A%22https%3A//buildbot.libretro.com/stable%22%2C%22author%22%3A%22buildbot.libretro.com%22%2C%22name%22%3A%22RetroArch%20%28AArch64%29%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%7B%5C%22customLinkFilterRegex%5C%22%3A%5C%22/stable/%5C%5C%5C%5Cd%2B.%5C%5C%5C%5Cd%2B.%5C%5C%5C%5Cd%2B/%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%7D%2C%7B%5C%22customLinkFilterRegex%5C%22%3A%5C%22/stable/%5C%5C%5C%5Cd%2B.%5C%5C%5C%5Cd%2B.%5C%5C%5C%5Cd%2B/android/%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%7D%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22/stable/%5C%5C%5C%5Cd%2B.%5C%5C%5C%5Cd%2B.%5C%5C%5C%5Cd%2B/android/RetroArch_aarch64.apk%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Atrue%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla/5.0%20%28Linux%3B%20Android%2010%3B%20K%29%20AppleWebKit/537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome/114.0.0.0%20Mobile%20Safari/537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22APKLinkHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%5C%5C%5Cd%2B.%5C%5C%5C%5Cd%2B.%5C%5C%5C%5Cd%2B%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22RetroArch%20AArch64%5C%22%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Atrue%2C%5C%22skipUpdateNotifications%5C%22%3Atrue%2C%5C%22about%5C%22%3A%5C%22Released%20less%20frequently.%20Better%20stability.%20%5C%22%2C%5C%22supportFixedAPKURL%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22HTML%22%2C%22meta%22%3A%7B%22nameOverride%22%3A%22RetroArch%20%28AArch64%29%22%2C%22urlOverride%22%3A%22https%3A//www.retroarch.com%22%7D%7D
RetroArch (AArch64): http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22487343355%22%2C%22url%22%3A%22https%3A//buildbot.libretro.com/nightly/android%22%2C%22author%22%3A%22buildbot.libretro.com%22%2C%22name%22%3A%22RetroArch%20%28AArch64%29%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%5C%5C%5Cd%7B4%7D-%5C%5C%5C%5Cd%7B2%7D-%5C%5C%5C%5Cd%7B2%7D-RetroArch_aarch64.apk%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Atrue%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla/5.0%20%28Linux%3B%20Android%2010%3B%20K%29%20AppleWebKit/537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome/114.0.0.0%20Mobile%20Safari/537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22APKLinkHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%5C%5C%5Cd%7B4%7D-%5C%5C%5C%5Cd%7B2%7D-%5C%5C%5C%5Cd%7B2%7D%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%5C%5C%5Cd%7B4%7D-%5C%5C%5C%5Cd%7B2%7D-%5C%5C%5C%5Cd%7B2%7D-RetroArch_aarch64.apk%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22RetroArch%20AArch64%20%28Nightly%29%5C%22%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22Nightly%20releases.%20cutting%20edge%20features%20but%20may%20contain%20bugs%5C%22%2C%5C%22supportFixedAPKURL%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22HTML%22%2C%22meta%22%3A%7B%22nameOverride%22%3A%22RetroArch%20Nightly%20%28AArch64%29%22%2C%22excludeFromExport%22%3Atrue%2C%22urlOverride%22%3A%22https%3A//www.retroarch.com%22%7D%7D
RPCSX: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22net.rpcsx%22%2C%22url%22%3A%22https%3A//github.com/RPCSX/rpcsx-ui-android%22%2C%22author%22%3A%22RPCSX%22%2C%22name%22%3A%22RPCSX%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
ScummVM: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.scummvm.scummvm%22%2C%22url%22%3A%22https%3A//downloads.scummvm.org/frs/scummvm/%22%2C%22author%22%3A%22ScummVM%22%2C%22name%22%3A%22ScummVM%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%7B%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%5C%5C%5Cd%2B%5C%5C%5C%5C.%5C%5C%5C%5Cd%2B%5C%5C%5C%5C.%5C%5C%5C%5Cd%2B%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%7D%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla/5.0%20%28Linux%3B%20Android%2010%3B%20K%29%20AppleWebKit/537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome/114.0.0.0%20Mobile%20Safari/537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22APKLinkHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%5C%5C%5Cd%2B%5C%5C%5C%5C.%5C%5C%5C%5Cd%2B%5C%5C%5C%5C.%5C%5C%5C%5Cd%2B%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22.%2Aandroid-arm64-v8a.apk%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22A%20program%20which%20allows%20you%20to%20run%20certain%20classic%20graphical%20adventure%20and%20role-playing%20games%2C%20provided%20you%20already%20have%20their%20data%20files.%5C%22%2C%5C%22supportFixedAPKURL%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22HTML%22%2C%22meta%22%3A%7B%22urlOverride%22%3A%22https%3A//www.scummvm.org%22%7D%7D
GameHub Lite: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22gamehub.lite%22%2C%22url%22%3A%22https%3A//github.com/Producdevity/gamehub-lite%22%2C%22author%22%3A%22Producdevity%22%2C%22name%22%3A%22GameHub%20Lite%22%2C%22preferredApkIndex%22%3A4%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22PC%20Emulation%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%7D
GameNative: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22app.gamenative%22%2C%22url%22%3A%22https%3A//github.com/utkarshdalal/GameNative%22%2C%22author%22%3A%22utkarshdalal%22%2C%22name%22%3A%22GameNative%22%2C%22preferredApkIndex%22%3A3%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22PC%20Emulation%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
Winlator: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.winlator%22%2C%22url%22%3A%22https%3A//github.com/brunodev85/winlator%22%2C%22author%22%3A%22brunodev85%22%2C%22name%22%3A%22Winlator%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22PC%20Emulation%22%5D%7D
winlator: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.winlator.cmod%22%2C%22url%22%3A%22https%3A//github.com/coffincolors/winlator%22%2C%22author%22%3A%22coffincolors%22%2C%22name%22%3A%22winlator%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Winlator%20Cmod%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22PC%20Emulation%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Atrue%2C%22meta%22%3A%7B%22nameOverride%22%3A%22Winlator%20CMod%22%7D%7D
Winlator-Ludashi: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.miHoYo.GenshinImpact%22%2C%22url%22%3A%22https%3A//github.com/StevenMXZ/Winlator-Ludashi%22%2C%22author%22%3A%22StevenMXZ%22%2C%22name%22%3A%22Winlator-Ludashi%22%2C%22preferredApkIndex%22%3A2%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22PC%20Emulation%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%7D
Vita3K: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.vita3k.emulator%22%2C%22url%22%3A%22https%3A//github.com/Vita3K/Vita3K-Android%22%2C%22author%22%3A%22Vita3K%22%2C%22name%22%3A%22Vita3K%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%7D
Vita3K ZX: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.vita3k.emulator.ikhoeyZX%22%2C%22url%22%3A%22https%3A//github.com/ikhoeyZX/Vita3K-Android%22%2C%22author%22%3A%22ikhoeyZX%22%2C%22name%22%3A%22Vita3K%20ZX%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
NetherSX2 Classic: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22xyz.aethersx2.android%22%2C%22url%22%3A%22https%3A//github.com/Trixarian/NetherSX2-classic%22%2C%22author%22%3A%22Trixarian%22%2C%22name%22%3A%22NetherSX2%20Classic%22%2C%22preferredApkIndex%22%3A1%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22NetherSX2-Classic%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22meta%22%3A%7B%22nameOverride%22%3A%22NetherSX2-Classic%22%7D%7D
NetherSX2: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22xyz.aethersx2.android%22%2C%22url%22%3A%22https%3A//github.com/Trixarian/NetherSX2-patch%22%2C%22author%22%3A%22Trixarian%22%2C%22name%22%3A%22NetherSX2%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22NetherSX2-Patch%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22meta%22%3A%7B%22nameOverride%22%3A%22NetherSX2-Patch%22%2C%22excludeFromExport%22%3Atrue%7D%7D
Artemis: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.limelight.noir%22%2C%22url%22%3A%22https%3A//github.com/ClassicOldSong/moonlight-android%22%2C%22author%22%3A%22ClassicOldSong%22%2C%22name%22%3A%22Artemis%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Streaming%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
Moonlight: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.limelight%22%2C%22url%22%3A%22https%3A//github.com/moonlight-stream/moonlight-android%22%2C%22author%22%3A%22moonlight-stream%22%2C%22name%22%3A%22Moonlight%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Streaming%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
Syncthing-Fork: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.github.catfriend1.syncthingfork%22%2C%22url%22%3A%22https%3A//github.com/researchxxl/syncthing-android%22%2C%22author%22%3A%22researchxxl%22%2C%22name%22%3A%22Syncthing-Fork%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Utilities%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%7D
ES-DE Android Apps: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.quantumsoul.esde_android%22%2C%22url%22%3A%22https%3A//github.com/BinaryQuantumSoul/esde_android_apps%22%2C%22author%22%3A%22BinaryQuantumSoul%22%2C%22name%22%3A%22ES-DE%20Android%20Apps%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Utilities%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
AdrenoToolsDrivers: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22476086958%22%2C%22url%22%3A%22https%3A//github.com/K11MCH1/AdrenoToolsDrivers%22%2C%22author%22%3A%22K11MCH1%22%2C%22name%22%3A%22AdrenoToolsDrivers%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22Turnip%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Atrue%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Afalse%2C%5C%22appName%5C%22%3A%5C%22Turnip%20Drivers%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22github-creds%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Track%20Only%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
Obtainium Emulation Pack: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22904332840%22%2C%22url%22%3A%22https%3A//github.com/RJNY/Obtainium-Emulation-Pack%22%2C%22author%22%3A%22RJNY%22%2C%22name%22%3A%22Obtainium%20Emulation%20Pack%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Atrue%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Obtainium%20Emulation%20Pack%5C%22%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Track%20Only%22%5D%7D
Daijishō: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.magneticchen.daijishou%22%2C%22url%22%3A%22https%3A//github.com/TapiocaFox/Daijishou%22%2C%22author%22%3A%22TapiocaFox%22%2C%22name%22%3A%22Daijish%5Cu014d%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Frontend%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
Pegasus: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22org.pegasus_frontend.android%22%2C%22url%22%3A%22https%3A//github.com/mmatyas/pegasus-frontend%22%2C%22author%22%3A%22mmatyas%22%2C%22name%22%3A%22Pegasus%22%2C%22preferredApkIndex%22%3A1%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Frontend%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
OdinTools: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22de.langerhans.odintools%22%2C%22url%22%3A%22https%3A//github.com/langerhans/OdinTools%22%2C%22author%22%3A%22langerhans%22%2C%22name%22%3A%22OdinTools%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Utilities%22%5D%2C%22overrideSource%22%3A%22GitHub%22%7D
MelonDS: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22me.magnum.melonds%22%2C%22url%22%3A%22https%3A//github.com/SapphireRhodonite/melonDS-android%22%2C%22author%22%3A%22SapphireRhodonite%22%2C%22name%22%3A%22MelonDS%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22MelonDS%20%28DS%29%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22categories%22%3A%5B%22Dual%20Screen%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%2C%22meta%22%3A%7B%22includeInStandard%22%3Afalse%2C%22includeInDualScreen%22%3Atrue%7D%7D
Cemu: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22info.cemu.cemu%22%2C%22url%22%3A%22https%3A//github.com/SapphireRhodonite/Cemu%22%2C%22author%22%3A%22SapphireRhodonite%22%2C%22name%22%3A%22Cemu%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Cemu%20%28DS%29%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22github-creds%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Dual%20Screen%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Afalse%2C%22meta%22%3A%7B%22includeInStandard%22%3Afalse%2C%22includeInDualScreen%22%3Atrue%7D%7D
ARMSX2: http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22come.nanodata.armsx2%22%2C%22url%22%3A%22https%3A//github.com/ARMSX2/ARMSX2%22%2C%22author%22%3A%22ARMSX2%22%2C%22name%22%3A%22ARMSX2%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22categories%22%3A%5B%22Emulator%22%5D%2C%22overrideSource%22%3A%22GitHub%22%2C%22allowIdChange%22%3Atrue%7D

View File

@@ -9,30 +9,19 @@ from utils import should_include_app
def minify_json(input_file: str, output_file: str, variant: str = "standard") -> None:
"""Filter apps by variant, remove meta fields, and output minified JSON.
Args:
input_file: Path to source applications.json
output_file: Path to write minified JSON
variant: One of 'standard' or 'dual-screen'
"""
try:
# Read JSON data from input file
with open(input_file, "r", encoding="utf-8") as f:
data: dict[str, Any] = json.load(f)
# Filter apps based on variant
if "apps" in data:
filtered_apps = []
for app in data["apps"]:
if should_include_app(app, variant):
# Remove meta key from export
app_copy = app.copy()
app_copy.pop("meta", None)
filtered_apps.append(app_copy)
data["apps"] = filtered_apps
# Minify JSON and write to output file
with open(output_file, "w", encoding="utf-8") as f:
json.dump(data, f, separators=(",", ":"), ensure_ascii=False)

View File

@@ -26,11 +26,12 @@ import shutil
import subprocess
import sys
import tempfile
import urllib.parse
from collections import defaultdict
from pathlib import Path
from typing import Any
from utils import get_application_url, get_display_name, make_obtainium_link, should_include_app
REPO_ROOT = Path(__file__).resolve().parent.parent
# Release artifact paths (relative to repo root)
@@ -41,8 +42,6 @@ APPLICATIONS_JSON = REPO_ROOT / "src" / "applications.json"
SEMVER_PATTERN = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$")
# Helpers
def run(cmd: list[str], capture: bool = False, check: bool = True) -> subprocess.CompletedProcess:
return subprocess.run(
cmd,
@@ -65,8 +64,6 @@ def check_prerequisites() -> None:
sys.exit(1)
# Version helpers
def get_latest_tag() -> str | None:
result = run(["git", "tag", "--sort=-v:refname"], capture=True, check=False)
if result.returncode != 0:
@@ -135,8 +132,6 @@ def prompt_version(latest: str | None) -> str:
print("Invalid choice. Enter 1, 2, 3, or 4.")
# App diff detection
def load_apps_from_ref(ref: str) -> dict[str, dict[str, Any]]:
result = run(
["git", "show", f"{ref}:src/applications.json"],
@@ -192,44 +187,7 @@ def diff_apps(
return added, changed, removed
# Obtainium link generation (mirrors generate-table.py)
def make_obtainium_link(app: dict[str, Any]) -> str:
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": app.get("additionalSettings"),
"categories": app.get("categories"),
"overrideSource": app.get("overrideSource"),
"allowIdChange": app.get("allowIdChange"),
}
encoded = urllib.parse.quote(json.dumps(payload, separators=(",", ":")), safe="")
return f"http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/{encoded}"
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", "")
# Table rendering for release notes
def make_app_table_row(app: dict[str, Any]) -> str:
display_name = f'<a href="{get_application_url(app)}">{get_display_name(app)}</a>'
@@ -273,8 +231,6 @@ def generate_app_table(apps: list[dict[str, Any]], group_by_category: bool = Fal
return "\n".join(sections)
# Commit log
def get_commit_summaries(since_tag: str | None) -> list[str]:
if since_tag:
cmd = ["git", "log", f"{since_tag}..HEAD", "--pretty=format:%s"]
@@ -288,8 +244,6 @@ def get_commit_summaries(since_tag: str | None) -> list[str]:
return [line.strip() for line in result.stdout.strip().splitlines() if line.strip()]
# Release notes
def generate_release_notes(
latest_tag: str | None,
added: list[dict[str, Any]],
@@ -352,8 +306,6 @@ def edit_release_notes(notes: str) -> str:
return edited
# Git / GitHub
def check_working_tree_clean() -> bool:
result = run(["git", "status", "--porcelain"], capture=True)
return result.stdout.strip() == ""
@@ -411,8 +363,6 @@ def get_app_count(json_path: Path) -> int:
return 0
# Main
def main() -> None:
parser = argparse.ArgumentParser(
description="Create a GitHub release for Obtainium Emulation Pack"

622
scripts/test-apps.py Normal file
View File

@@ -0,0 +1,622 @@
#!/usr/bin/env python3
"""Live validation that app configs can resolve to downloadable APKs.
Usage:
python scripts/test-apps.py src/applications.json
python scripts/test-apps.py src/applications.json Dolphin
python scripts/test-apps.py src/applications.json --id org.dolphinemu.dolphinemu
Set GITHUB_TOKEN in .env or environment to avoid API rate limits.
"""
import json
import os
import re
import ssl
import sys
import time
from html.parser import HTMLParser
from typing import Any
from urllib.parse import urljoin, urlparse
from urllib.request import Request, urlopen
from utils import load_dotenv
USER_AGENT = (
"Mozilla/5.0 (Linux; Android 10; K) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/114.0.0.0 Mobile Safari/537.36"
)
REQUEST_TIMEOUT = 30
MAX_RELEASES_TO_CHECK = 25
APK_EXTENSIONS = (".apk", ".xapk")
MAX_STORED_APK_URLS = 5
MAX_DISPLAYED_APK_URLS = 3
def _make_request(
url: str,
headers: dict[str, str] | None = None,
timeout: int = REQUEST_TIMEOUT,
) -> tuple[str, dict[str, str], str]:
"""Returns (body, response_headers, final_url). Allows self-signed certs."""
hdrs = {"User-Agent": USER_AGENT}
if headers:
hdrs.update(headers)
req = Request(url, headers=hdrs)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
resp = urlopen(req, timeout=timeout, context=ctx)
body = resp.read().decode("utf-8", errors="replace")
resp_headers = {k.lower(): v for k, v in resp.headers.items()}
return body, resp_headers, resp.url
def _fetch_json(
url: str,
headers: dict[str, str] | None = None,
) -> tuple[Any, dict[str, str]]:
hdrs = {"Accept": "application/json"}
if headers:
hdrs.update(headers)
body, resp_headers, _ = _make_request(url, headers=hdrs)
return json.loads(body), resp_headers
class LinkExtractor(HTMLParser):
def __init__(self, base_url: str):
super().__init__()
self.base_url = base_url
self.links: list[str] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == "a":
for name, value in attrs:
if name == "href" and value:
self.links.append(urljoin(self.base_url, value))
def _extract_links(html_body: str, base_url: str) -> list[str]:
parser = LinkExtractor(base_url)
parser.feed(html_body)
return parser.links
def _filter_links_by_regex(links: list[str], regex: str) -> list[str]:
pattern = re.compile(regex)
return [link for link in links if pattern.search(link)]
def _filter_links_by_extension(links: list[str]) -> list[str]:
return [link for link in links if any(link.lower().endswith(ext) for ext in APK_EXTENSIONS)]
def _sort_links(
links: list[str],
skip_sort: bool = False,
reverse_sort: bool = False,
sort_by_last_segment: bool = False,
) -> list[str]:
if skip_sort:
return links
key = (lambda u: u.rsplit("/", 1)[-1]) if sort_by_last_segment else None
result = sorted(links, key=key)
if reverse_sort:
result.reverse()
return result
def _format_filter_context(**filters: str) -> str:
"""Build a diagnostic string of active filters, e.g. ', apkFilter=foo, titleFilter=bar'."""
parts = [f", {name}={value}" for name, value in filters.items() if value]
return "".join(parts)
def _apply_apk_filter(urls: list[str], settings: dict[str, Any]) -> list[str]:
apk_filter = settings.get("apkFilterRegEx", "")
if not apk_filter or not urls:
return urls
pattern = re.compile(apk_filter)
if settings.get("invertAPKFilter", False):
return [u for u in urls if not pattern.search(u)]
return [u for u in urls if pattern.search(u)]
def _extract_version(raw_version: str, settings: dict[str, Any]) -> tuple[str, str | None]:
"""Apply versionExtractionRegEx. Returns (version, warning_or_none)."""
regex_str = settings.get("versionExtractionRegEx", "")
if not regex_str or not raw_version:
return raw_version, None
try:
match = re.search(regex_str, raw_version)
if match:
group_to_use = settings.get("matchGroupToUse", "")
if group_to_use:
return match.expand(group_to_use), None
elif match.groups():
return match.group(1), None
return match.group(0), None
except re.error as e:
return raw_version, f"versionExtractionRegEx error: {e}"
return raw_version, None
def _check_apk_index(app: dict[str, Any], apk_count: int) -> str | None:
"""Returns a warning string if preferredApkIndex is out of bounds."""
index = app.get("preferredApkIndex", 0)
if apk_count > 0 and index >= apk_count:
return f"preferredApkIndex={index} but only {apk_count} APKs found"
return None
class TestResult:
def __init__(self, app_name: str, app_id: str, source: str, url: str):
self.app_name = app_name
self.app_id = app_id
self.source = source
self.url = url
self.passed = False
self.version: str | None = None
self.apk_count = 0
self.apk_urls: list[str] = []
self.error: str | None = None
self.warnings: list[str] = []
self.duration_ms = 0
def __repr__(self) -> str:
status = "PASS" if self.passed else "FAIL"
return f"{status}: {self.app_name} ({self.source})"
def _github_headers() -> dict[str, str]:
headers = {"Accept": "application/vnd.github.v3+json"}
token = os.environ.get("GITHUB_TOKEN", "")
if token:
headers["Authorization"] = f"token {token}"
return headers
def _parse_owner_repo(url: str) -> tuple[str, str, str]:
"""Returns (owner, repo, host)."""
parsed = urlparse(url)
parts = parsed.path.strip("/").split("/")
if len(parts) < 2:
raise ValueError(f"Cannot parse owner/repo from: {url}")
return parts[0], parts[1], parsed.netloc
def _collect_apks_from_assets(assets: list[dict], settings: dict[str, Any]) -> list[str]:
"""Extract APK download URLs from a GitHub/Codeberg release's asset list."""
urls = []
for asset in assets:
name = asset.get("name", "").lower()
dl_url = asset.get("browser_download_url", "")
if any(name.endswith(ext) for ext in APK_EXTENSIONS):
urls.append(dl_url)
elif name.endswith(".zip") and settings.get("includeZips", False):
urls.append(dl_url)
return urls
def _find_release_with_apks(
releases: list[dict],
settings: dict[str, Any],
title_filter: re.Pattern | None = None,
notes_filter: re.Pattern | None = None,
) -> tuple[dict | None, list[str]]:
"""Walk releases and return the first one with matching APK assets.
Returns (target_release, filtered_apk_urls). For track-only apps,
falls back to any release with a tag_name even if no APKs found.
"""
include_prereleases = settings.get("includePrereleases", False)
track_only = settings.get("trackOnly", False)
fallback = settings.get("fallbackToOlderReleases", True)
for release in releases:
if release.get("draft", False):
continue
if release.get("prerelease", False) and not include_prereleases:
continue
if title_filter:
name = release.get("name", "") or ""
if not title_filter.search(name):
continue
if notes_filter:
body = release.get("body", "") or ""
if not notes_filter.search(body):
continue
apk_urls = _collect_apks_from_assets(release.get("assets", []), settings)
apk_urls = _apply_apk_filter(apk_urls, settings)
if not apk_urls and not track_only:
if fallback:
continue
break
return release, apk_urls
# Track-only fallback: any release with a version tag
if track_only:
for release in releases:
if release.get("tag_name"):
return release, []
return None, []
def test_github(app: dict[str, Any], settings: dict[str, Any]) -> TestResult:
result = TestResult(app["name"], app["id"], "GitHub", app["url"])
try:
owner, repo, _ = _parse_owner_repo(app["url"])
except ValueError as e:
result.error = str(e)
return result
api_url = f"https://api.github.com/repos/{owner}/{repo}/releases?per_page={MAX_RELEASES_TO_CHECK}"
try:
releases, resp_headers = _fetch_json(api_url, headers=_github_headers())
except Exception as e:
result.error = f"GitHub API error: {e}"
if "403" in str(e) or "rate" in str(e).lower():
result.error += " (rate limited - set GITHUB_TOKEN env var)"
return result
remaining = resp_headers.get("x-ratelimit-remaining", "")
if remaining and int(remaining) < 10:
result.warnings.append(f"GitHub API rate limit low: {remaining} remaining")
if not releases:
result.error = "No releases found"
return result
title_str = settings.get("filterReleaseTitlesByRegEx", "")
notes_str = settings.get("filterReleaseNotesByRegEx", "")
title_regex = re.compile(title_str) if title_str else None
notes_regex = re.compile(notes_str) if notes_str else None
target, apk_urls = _find_release_with_apks(
releases, settings, title_filter=title_regex, notes_filter=notes_regex
)
if not target:
prerelease_state = "on" if settings.get("includePrereleases", False) else "off"
context = _format_filter_context(
titleFilter=title_str,
apkFilter=settings.get("apkFilterRegEx", ""),
)
result.error = (
f"No releases with matching APK assets found "
f"(checked {len(releases)} releases, prereleases={prerelease_state}{context})"
)
return result
version = target.get("tag_name", "") or target.get("name", "")
version, warning = _extract_version(version, settings)
if warning:
result.warnings.append(warning)
index_warning = _check_apk_index(app, len(apk_urls))
if index_warning:
result.warnings.append(index_warning)
result.passed = True
result.version = version
result.apk_count = len(apk_urls)
result.apk_urls = apk_urls
return result
def test_codeberg(app: dict[str, Any], settings: dict[str, Any]) -> TestResult:
result = TestResult(app["name"], app["id"], "Codeberg", app["url"])
try:
owner, repo, host = _parse_owner_repo(app["url"])
except ValueError as e:
result.error = str(e)
return result
api_url = f"https://{host}/api/v1/repos/{owner}/{repo}/releases?limit={MAX_RELEASES_TO_CHECK}"
try:
releases, _ = _fetch_json(api_url)
except Exception as e:
result.error = f"Codeberg API error: {e}"
return result
if not releases:
result.error = "No releases found"
return result
target, apk_urls = _find_release_with_apks(releases, settings)
if not target:
result.error = "No releases with matching APK assets"
return result
version = target.get("tag_name", "") or target.get("name", "")
version, warning = _extract_version(version, settings)
if warning:
result.warnings.append(warning)
index_warning = _check_apk_index(app, len(apk_urls))
if index_warning:
result.warnings.append(index_warning)
result.passed = True
result.version = version
result.apk_count = len(apk_urls)
result.apk_urls = apk_urls
return result
def _parse_request_headers(settings: dict[str, Any]) -> dict[str, str]:
headers = {}
for header_obj in settings.get("requestHeader", []):
if isinstance(header_obj, dict):
header_str = header_obj.get("requestHeader", "")
if ": " in header_str:
key, val = header_str.split(": ", 1)
headers[key] = val
return headers
def _follow_intermediate_links(
start_url: str,
steps: list[dict],
headers: dict[str, str],
) -> tuple[str, str | None]:
"""Walk intermediateLink chain. Returns (final_url, error_or_none)."""
current_url = start_url
for i, step in enumerate(steps):
if not isinstance(step, dict):
continue
try:
body, _, final_url = _make_request(current_url, headers=headers)
except Exception as e:
return current_url, f"Failed to fetch intermediate URL ({current_url}): {e}"
links = _extract_links(body, final_url)
step_regex = step.get("customLinkFilterRegex", "")
if step_regex:
links = _filter_links_by_regex(links, step_regex)
links = _sort_links(
links,
skip_sort=step.get("skipSort", False),
reverse_sort=step.get("reverseSort", False),
sort_by_last_segment=step.get("sortByLastLinkSegment", False),
)
if not links:
return current_url, (
f"Intermediate link step {i} found no matching links "
f"(url={current_url}, regex={step_regex!r})"
)
current_url = links[-1] # Obtainium takes the last link after sorting
return current_url, None
def test_html(app: dict[str, Any], settings: dict[str, Any]) -> TestResult:
result = TestResult(app["name"], app["id"], "HTML", app["url"])
req_headers = _parse_request_headers(settings)
intermediate_links = settings.get("intermediateLink", [])
current_url, error = _follow_intermediate_links(app["url"], intermediate_links, req_headers)
if error:
result.error = error
return result
try:
body, _, final_url = _make_request(current_url, headers=req_headers)
except Exception as e:
result.error = f"Failed to fetch final URL ({current_url}): {e}"
return result
links = _extract_links(body, final_url)
custom_regex = settings.get("customLinkFilterRegex", "")
apk_links = _filter_links_by_regex(links, custom_regex) if custom_regex else _filter_links_by_extension(links)
apk_links = _apply_apk_filter(apk_links, settings)
track_only = settings.get("trackOnly", False)
if not apk_links and not track_only:
context = _format_filter_context(
customLinkFilterRegex=custom_regex,
apkFilterRegEx=settings.get("apkFilterRegEx", ""),
)
result.error = (
f"No APK links found on page ({current_url}{context}, "
f"{len(links)} total links on page)"
)
return result
version = None
version_regex_str = settings.get("versionExtractionRegEx", "")
if version_regex_str:
extract_whole_page = settings.get("versionExtractWholePage", False)
if extract_whole_page:
search_text = body
elif apk_links:
search_text = apk_links[-1] # Obtainium uses last link
else:
search_text = ""
version, warning = _extract_version(search_text, settings)
if warning:
result.warnings.append(warning)
if not version:
pseudo_method = settings.get("defaultPseudoVersioningMethod", "")
if pseudo_method:
version = f"<pseudo:{pseudo_method}>"
else:
result.warnings.append("No version extracted (no regex match, no pseudo-method)")
index_warning = _check_apk_index(app, len(apk_links))
if index_warning:
result.warnings.append(index_warning)
result.passed = True
result.version = version
result.apk_count = len(apk_links)
result.apk_urls = apk_links[:MAX_STORED_APK_URLS]
return result
def _effective_source(app: dict[str, Any]) -> str:
override = app.get("overrideSource")
if override:
return override
host = urlparse(app.get("url", "")).netloc.lower().lstrip("www.")
if "github.com" in host:
return "GitHub"
if "gitlab.com" in host:
return "GitLab"
if "codeberg.org" in host:
return "Codeberg"
if "f-droid.org" in host:
return "FDroid"
return "HTML"
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:
result = TestResult(app.get("name", "?"), app.get("id", "?"), source, app.get("url", "?"))
result.error = "Cannot parse additionalSettings JSON"
return result
start = time.monotonic()
if source == "GitHub":
result = test_github(app, settings)
elif source == "Codeberg":
result = test_codeberg(app, settings)
elif source in ("HTML", "DirectAPKLink"):
result = test_html(app, settings)
else:
result = TestResult(app.get("name", "?"), app.get("id", "?"), source, app.get("url", "?"))
result.passed = True
result.warnings.append(f"Skipped: source type '{source}' not yet supported")
result.duration_ms = int((time.monotonic() - start) * 1000)
return result
def print_result(result: TestResult, verbose: bool = False) -> None:
status = "\033[32mPASS\033[0m" if result.passed else "\033[31mFAIL\033[0m"
version_str = f" v{result.version}" if result.version else ""
apk_str = f" ({result.apk_count} APKs)" if result.apk_count else ""
print(f" {status} {result.app_name}{version_str}{apk_str} [{result.duration_ms}ms]")
if result.error:
print(f" Error: {result.error}")
for warning in result.warnings:
print(f" \033[33mWarn\033[0m: {warning}")
if verbose and result.apk_urls:
for url in result.apk_urls[:MAX_DISPLAYED_APK_URLS]:
print(f" APK: {url}")
def main() -> int:
load_dotenv()
if len(sys.argv) < 2:
print("Usage: python test-apps.py <json_file> [name_filter] [--id <app_id>] [--verbose]")
print()
print("Examples:")
print(" python test-apps.py src/applications.json # test all apps")
print(" python test-apps.py src/applications.json Dolphin # filter by name")
print(" python test-apps.py src/applications.json --id org.dolphinemu.dolphinemu")
print(" python test-apps.py src/applications.json --verbose # show APK URLs")
return 1
json_file = sys.argv[1]
args = sys.argv[2:]
verbose = "--verbose" in args
if verbose:
args.remove("--verbose")
id_filter = None
if "--id" in args:
idx = args.index("--id")
if idx + 1 < len(args):
id_filter = args[idx + 1]
args = args[:idx] + args[idx + 2:]
else:
print("Error: --id requires an argument")
return 1
name_filter = " ".join(args).lower() if args else None
try:
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"Error loading {json_file}: {e}")
return 1
apps = data.get("apps", [])
if id_filter:
apps = [a for a in apps if a.get("id") == id_filter]
elif name_filter:
apps = [a for a in apps if name_filter in a.get("name", "").lower()]
if not apps:
print("No apps matched the filter.")
return 1
has_token = bool(os.environ.get("GITHUB_TOKEN"))
github_count = sum(1 for a in apps if _effective_source(a) == "GitHub")
if github_count > 0 and not has_token:
print(
f"\033[33mNote\033[0m: {github_count} GitHub apps to test, "
"but GITHUB_TOKEN is not set. You may hit rate limits.\n"
" Set it with: export GITHUB_TOKEN=<your_token>\n"
)
print(f"Testing {len(apps)} app(s)...\n")
results = []
for app in apps:
result = test_app(app)
results.append(result)
print_result(result, verbose=verbose)
passed = sum(1 for r in results if r.passed)
failed = sum(1 for r in results if not r.passed)
warned = sum(1 for r in results if r.warnings)
total_time = sum(r.duration_ms for r in results)
print(f"\n{'=' * 60}")
print(f"Results: {passed} passed, {failed} failed, {warned} with warnings")
print(f"Time: {total_time / 1000:.1f}s total")
if failed > 0:
print(f"\nFailed apps:")
for r in results:
if not r.passed:
print(f" - {r.app_name}: {r.error}")
return 1 if failed > 0 else 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,40 +1,64 @@
"""Shared utility functions for Obtainium Emulation Pack scripts."""
import json
import os
import urllib.parse
from pathlib import Path
from typing import Any
from constants import OBTAINIUM_SCHEME, REDIRECT_URL
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 should_include_app(app: dict[str, Any], variant: str) -> bool:
"""Determine if an app should be included based on variant and meta fields.
Args:
app: Application dictionary from applications.json
variant: One of 'standard' or 'dual-screen'
Returns:
True if the app should be included in the specified variant
"""
meta = app.get("meta", {})
# HIGHEST PRIORITY: Global exclusion overrides everything
if meta.get("excludeFromExport", False):
return False
# SECOND PRIORITY: Variant-specific inclusion/exclusion
if variant == "standard":
# Default: include in standard
return meta.get("includeInStandard", True)
elif variant == "dual-screen":
# Default: include in dual screen
return meta.get("includeInDualScreen", True)
return True
def get_display_name(app: dict[str, Any]) -> str:
"""Get the display name for an app, respecting nameOverride."""
return app.get("meta", {}).get("nameOverride") or app.get("name", "")
def get_application_url(app: dict[str, Any]) -> str:
"""Get the URL for an app, respecting urlOverride."""
return app.get("meta", {}).get("urlOverride") or app.get("url", "")
def make_obtainium_link(app: dict[str, Any]) -> str:
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": app.get("additionalSettings"),
"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}"

View File

@@ -1,17 +1,23 @@
"""Validate applications.json against schema and check for common issues."""
import json
import re
import sys
from collections import defaultdict
from typing import Any
from urllib.parse import urlparse
from constants import VARIANTS
from constants import (
COMMON_SETTINGS_KEYS,
REGEX_SETTINGS_KEYS,
SOURCE_HOST_MAP,
SOURCE_SPECIFIC_KEYS,
VALID_SOURCES,
VARIANTS,
)
from utils import should_include_app
# Required fields for each app
REQUIRED_FIELDS = {"id", "url", "author", "name"}
# Valid meta keys
VALID_META_KEYS = {
"excludeFromExport",
"excludeFromTable",
@@ -21,55 +27,206 @@ VALID_META_KEYS = {
"includeInDualScreen",
}
META_TYPO_MAP = {
"exludeFromExport": "excludeFromExport",
"exludeFromTable": "excludeFromTable",
"nameOveride": "nameOverride",
"urlOveride": "urlOverride",
}
def validate_app(app: dict[str, Any], index: int) -> list[str]:
"""Validate a single app entry and return list of errors."""
def _check_regex(pattern: str, field_name: str, app_name: str) -> str | None:
if not pattern:
return None
try:
re.compile(pattern)
return None
except re.error as e:
return f"{app_name}: invalid regex in '{field_name}': {e} (pattern: {pattern!r})"
def _detect_source_from_url(url: str) -> str | None:
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 _valid_keys_for_source(source: str | None) -> set[str]:
valid = set(COMMON_SETTINGS_KEYS)
if source and source in SOURCE_SPECIFIC_KEYS:
valid |= SOURCE_SPECIFIC_KEYS[source]
return valid
def _validate_required_fields(app: dict, app_name: str) -> list[str]:
return [
f"{app_name}: missing required field '{f}'"
for f in REQUIRED_FIELDS
if f not in app
]
def _validate_url(app: dict, app_name: str) -> list[str]:
errors = []
app_name = app.get("name", f"app[{index}]")
url = app.get("url", "")
if not url:
return errors
try:
parsed = urlparse(url)
if not parsed.scheme:
errors.append(f"{app_name}: URL missing scheme (http/https): {url}")
elif parsed.scheme not in ("http", "https"):
errors.append(f"{app_name}: URL has non-http scheme: {parsed.scheme}")
if not parsed.netloc:
errors.append(f"{app_name}: URL missing host: {url}")
except Exception as e:
errors.append(f"{app_name}: malformed URL: {e}")
return errors
# Check required fields
for field in REQUIRED_FIELDS:
if field not in app:
errors.append(f"{app_name}: missing required field '{field}'")
# Validate meta keys if present
def _validate_override_source(
app: dict, app_name: str
) -> tuple[list[str], list[str]]:
errors, warnings = [], []
source = app.get("overrideSource")
url = app.get("url", "")
if source is not None and source not in VALID_SOURCES:
errors.append(
f"{app_name}: unknown overrideSource '{source}' "
f"(valid: {', '.join(sorted(VALID_SOURCES))})"
)
elif source is None:
warnings.append(f"{app_name}: missing overrideSource (auto-detection may be fragile)")
if url and source:
detected = _detect_source_from_url(url)
if detected and detected != source and source != "HTML" and detected != "HTML":
warnings.append(
f"{app_name}: URL host suggests '{detected}' but "
f"overrideSource is '{source}'"
)
return errors, warnings
def _validate_apk_index(app: dict, app_name: str) -> list[str]:
index = app.get("preferredApkIndex")
if index is not None and (not isinstance(index, int) or index < 0):
return [
f"{app_name}: preferredApkIndex must be a non-negative integer, got {index!r}"
]
return []
def _validate_meta(app: dict, app_name: str) -> list[str]:
errors = []
meta = app.get("meta", {})
for key in meta.keys():
for key in meta:
if key not in VALID_META_KEYS:
errors.append(f"{app_name}: unknown meta key '{key}' (typo?)")
# Check for common typos
typo_checks = {
"exludeFromExport": "excludeFromExport",
"exludeFromTable": "excludeFromTable",
"nameOveride": "nameOverride",
"urlOveride": "urlOverride",
}
for typo, correct in typo_checks.items():
for typo, correct in META_TYPO_MAP.items():
if typo in meta:
errors.append(f"{app_name}: typo in meta key '{typo}', should be '{correct}'")
# Validate additionalSettings is valid inner JSON
additional_settings = app.get("additionalSettings")
if additional_settings is not None:
if not isinstance(additional_settings, str):
errors.append(f"{app_name}: 'additionalSettings' should be a JSON string")
else:
try:
json.loads(additional_settings)
except json.JSONDecodeError as e:
errors.append(f"{app_name}: 'additionalSettings' contains invalid JSON: {e}")
# Validate categories is a list
categories = app.get("categories")
if categories is not None and not isinstance(categories, list):
errors.append(f"{app_name}: 'categories' should be a list")
return errors
def _validate_categories(app: dict, app_name: str) -> list[str]:
categories = app.get("categories")
if categories is not None and not isinstance(categories, list):
return [f"{app_name}: 'categories' should be a list"]
return []
def _validate_additional_settings(
app: dict, app_name: str
) -> tuple[list[str], list[str]]:
errors, warnings = [], []
raw = app.get("additionalSettings")
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}")
return errors, warnings
if not isinstance(settings, dict):
return errors, warnings
# Validate regex fields
for key in REGEX_SETTINGS_KEYS:
value = settings.get(key, "")
if isinstance(value, str):
err = _check_regex(value, key, app_name)
if err:
errors.append(err)
# Validate regex in intermediate link steps
for i, link in enumerate(settings.get("intermediateLink", [])):
if isinstance(link, dict):
regex_val = link.get("customLinkFilterRegex", "")
if isinstance(regex_val, str):
err = _check_regex(regex_val, f"intermediateLink[{i}].customLinkFilterRegex", app_name)
if err:
errors.append(err)
# Check for source-inappropriate keys
url = app.get("url", "")
effective_source = app.get("overrideSource") or _detect_source_from_url(url)
if effective_source:
valid_keys = _valid_keys_for_source(effective_source)
for key in settings:
if key not in valid_keys:
belongs_to = [
s for s, keys in SOURCE_SPECIFIC_KEYS.items()
if key in keys and s != effective_source
]
if belongs_to:
warnings.append(
f"{app_name}: additionalSettings key '{key}' "
f"is for {'/'.join(belongs_to)}, not {effective_source}"
)
return errors, warnings
def validate_app(app: dict[str, Any], index: int) -> tuple[list[str], list[str]]:
errors, warnings = [], []
app_name = app.get("name", f"app[{index}]")
errors += _validate_required_fields(app, app_name)
errors += _validate_url(app, app_name)
src_errors, src_warnings = _validate_override_source(app, app_name)
errors += src_errors
warnings += src_warnings
errors += _validate_apk_index(app, app_name)
errors += _validate_meta(app, app_name)
errors += _validate_categories(app, app_name)
settings_errors, settings_warnings = _validate_additional_settings(app, app_name)
errors += settings_errors
warnings += settings_warnings
return errors, warnings
def check_duplicate_ids(apps: list[dict[str, Any]], variant: str) -> list[str]:
"""Check for duplicate app IDs within a variant."""
errors = []
ids_seen: dict[str, str] = {}
@@ -79,9 +236,8 @@ def check_duplicate_ids(apps: list[dict[str, Any]], variant: str) -> list[str]:
app_id = app.get("id", "")
app_name = app.get("name", "unknown")
if not app_id:
continue # Already caught by required field check
continue
if app_id in ids_seen:
errors.append(
@@ -95,7 +251,6 @@ def check_duplicate_ids(apps: list[dict[str, Any]], variant: str) -> list[str]:
def validate_json(input_file: str) -> int:
"""Validate applications.json and return exit code (0=success, 1=errors)."""
try:
with open(input_file, "r", encoding="utf-8") as f:
data = json.load(f)
@@ -111,25 +266,33 @@ def validate_json(input_file: str) -> int:
return 1
apps = data["apps"]
all_errors = []
all_errors, all_warnings = [], []
# Validate each app
for i, app in enumerate(apps):
errors = validate_app(app, i)
errors, warnings = validate_app(app, i)
all_errors.extend(errors)
all_warnings.extend(warnings)
# Check for duplicate IDs per variant
for variant in VARIANTS:
errors = check_duplicate_ids(apps, variant)
all_errors.extend(errors)
all_errors.extend(check_duplicate_ids(apps, variant))
if all_warnings:
print(f"Warnings ({len(all_warnings)}):\n")
for warning in all_warnings:
print(f" ~ {warning}")
print()
if all_errors:
print(f"Validation failed with {len(all_errors)} error(s):\n")
for error in all_errors:
print(f" - {error}")
print(f" x {error}")
return 1
print(f"Validation passed: {len(apps)} apps checked")
print(f"Validation passed: {len(apps)} apps checked", end="")
if all_warnings:
print(f" ({len(all_warnings)} warnings)")
else:
print()
return 0