diff --git a/.config/appimage/AppRun b/.config/appimage/AppRun new file mode 100644 index 0000000..7320ecc --- /dev/null +++ b/.config/appimage/AppRun @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$APPDIR/usr/bin/{{BINARY_NAME}}" "$@" \ No newline at end of file diff --git a/.config/appimage/com.simeonradivoev.gameflow-deck.appdata.xml b/.config/appimage/com.simeonradivoev.gameflow-deck.appdata.xml new file mode 100644 index 0000000..1d4cc0f --- /dev/null +++ b/.config/appimage/com.simeonradivoev.gameflow-deck.appdata.xml @@ -0,0 +1,53 @@ + + + {{APP_ID}} + CC0-1.0 + {{LICENSE}} + {{APP_NAME}} + Retro gaming frontend designed for handheld and controllers + + Simeon Radivoev + + +

A Cross-Platform Retro gaming frontend designed for handheld and controllers. Focused on building a simple user experience and intuitive UI.

+
+ + Game + + + always + + {{APP_ID}}.desktop + https://github.com/simeonradivoev/gameflow-deck + https://github.com/simeonradivoev/gameflow-deck/issues + https://github.com/sponsors/simeonradivoev + + + https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/yObFD2LySH.jpg + + + Game Details + https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/3nhuKCK6E3.jpg + + + The Settings Panel + https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/GL7SkQbHIY.png + + + Emulator Details + https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/xNj7scPEDQ.png + + + Gameflow Store + https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/CpBLzTNM6N.png + + + +{{{RELEASES}}} + + + + {{APP_ID}}.desktop + gameflow + +
\ No newline at end of file diff --git a/.config/appimage/com.simeonradivoev.gameflow-deck.desktop b/.config/appimage/com.simeonradivoev.gameflow-deck.desktop new file mode 100644 index 0000000..dddf26d --- /dev/null +++ b/.config/appimage/com.simeonradivoev.gameflow-deck.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +X-AppImage-Name={{APP_NAME}} +X-AppImage-Version={{VERSION}} +X-AppImage-Arch={{ARCH}} +Name={{APP_NAME}} +Comment={{DESCRIPTION}} +Exec=gameflow +Icon=gameflow +Type=Application +Categories=Game; \ No newline at end of file diff --git a/.config/flatpak/com.simeonradivoev.gameflow-deck.json b/.config/flatpak/com.simeonradivoev.gameflow-deck.json index dbaabd8..ebe1efa 100644 --- a/.config/flatpak/com.simeonradivoev.gameflow-deck.json +++ b/.config/flatpak/com.simeonradivoev.gameflow-deck.json @@ -1,23 +1,36 @@ { "app-id": "com.simeonradivoev.gameflow-deck", - "runtime": "org.kde.Platform", - "runtime-version": "6.10", - "sdk": "org.kde.Sdk", + "runtime": "org.freedesktop.Platform", + "runtime-version": "25.08", + "sdk": "org.freedesktop.Sdk", "command": "/app/bin/gameflow", - "base": "io.qt.qtwebengine.BaseApp", - "base-version": "6.10", "finish-args": [ "--share=ipc", "--share=network", "--socket=pulseaudio", "--socket=wayland", + "--socket=inherit-wayland-socket", "--socket=x11", + "--socket=fallback-x11", + "--socket=session-bus", + "--socket=system-bus", "--device=all", "--filesystem=host", "--filesystem=home", + "--filesystem=~/.steam/steam:rw", + "--filesystem=~/.steam:rw", + "--filesystem=~/.var/app/com.valvesoftware.Steam:rw", + "--filesystem=/run/udev:ro", + "--filesystem=/run/media", + "--filesystem=xdg-documents", + "--filesystem=xdg-desktop", + "--filesystem=xdg-run/gamescope-0:rw", "--env=PKG_CONFIG_LIBDIR=/app/lib", "--env=FLATPAK_BUILD=true", - "--allow=devel" + "--allow=devel", + "--talk-name=org.freedesktop.portal.OpenURI", + "--talk-name=org.freedesktop.Flatpak", + "--talk-name=org.a11y.Bus" ], "modules": [ { @@ -29,7 +42,6 @@ "mkdir -p /app/lib", "install -Dm644 256x256.png /app/share/icons/hicolor/256x256/apps/com.simeonradivoev.gameflow-deck.png", "install -Dm644 com.simeonradivoev.gameflow-deck.desktop /app/share/applications/com.simeonradivoev.gameflow-deck.desktop", - "mv libvips-cpp.so.* /app/lib", "mv * /app/share/gameflow/", "mv /app/share/gameflow/gameflow /app/bin", "mv /app/share/gameflow/bun /app/bin", @@ -39,15 +51,15 @@ "sources": [ { "type": "dir", - "path": "../build/linux" + "path": "../../build/linux" }, { "type": "file", - "path": "../flatpak/com.simeonradivoev.gameflow-deck.desktop" + "path": "com.simeonradivoev.gameflow-deck.desktop" }, { "type": "file", - "path": "../src/mainview/assets/256x256.png" + "path": "../../src/mainview/public/256x256.png" }, { "type": "script", @@ -72,23 +84,22 @@ "only-arches": [ "aarch64" ] - }, - { - "type": "file", - "path": "../node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3", - "only-arches": [ - "x86_64" - ] } ] }, { - "name": "webview", - "buildsystem": "cmake-ninja", + "name": "NW.js", + "buildsystem": "simple", + "build-commands": [ + "mkdir -p /app/bin/nw", + "mv * /app/bin/nw", + "chmod +x /app/bin/nw/nw" + ], "sources": [ { - "type": "dir", - "path": "../flatpak/webview" + "type": "archive", + "url": "https://dl.nwjs.io/v0.110.1/nwjs-v0.110.1-linux-x64.tar.gz", + "sha256": "d9a9ed2255e9ee87c9dd1860d9c7a479cea5279dcd80d3e80e23b083d325554a" } ] } diff --git a/.config/flatpak/webview/CMakeLists.txt b/.config/flatpak/webview/CMakeLists.txt deleted file mode 100644 index 511252a..0000000 --- a/.config/flatpak/webview/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -project(SimpleWebView LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Required for Qt WebEngine -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_AUTOUIC ON) - -find_package(Qt6 REQUIRED COMPONENTS - Core - Widgets - WebEngineWidgets, - Gamepad -) - -add_executable(webview - main.cpp -) - -target_link_libraries(webview PRIVATE - Qt6::Core - Qt6::Widgets - Qt6::WebEngineWidgets, - Qt6::Gamepad -) - - -# Install binary into Flatpak prefix (/app) -install(TARGETS webview - RUNTIME DESTINATION bin -) \ No newline at end of file diff --git a/.config/flatpak/webview/main.cpp b/.config/flatpak/webview/main.cpp deleted file mode 100644 index d0982e7..0000000 --- a/.config/flatpak/webview/main.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include -#include -#include -#include - -int main(int argc, char *argv[]) -{ - QApplication app(argc, argv); - if (argc < 2) { return 1; } - QWebEngineView view; - view.setUrl(QUrl(argv[1])); - view.showFullScreen(); - return app.exec(); -} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 0a0c0bd..01c3dfe 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,6 @@ *.gif filter=lfs diff=lfs merge=lfs -text *.webp filter=lfs diff=lfs merge=lfs -text *.svg filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.ogg filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.github/screenshots/3d screenshot.png b/.github/screenshots/3d screenshot.png new file mode 100644 index 0000000..6510ea1 --- /dev/null +++ b/.github/screenshots/3d screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b246781b29fd3ef53ace2639229cb2149b791bf82977cb481f3b4f343f8776d +size 246650 diff --git a/.github/screenshots/3nhuKCK6E3.png b/.github/screenshots/3nhuKCK6E3.png new file mode 100644 index 0000000..5feae4b --- /dev/null +++ b/.github/screenshots/3nhuKCK6E3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5be6551346cd1067ab4bbb172828a801c4e26f1c9cac9ce35e1e712356df05bb +size 1970162 diff --git a/.github/screenshots/4MtAe7Wkev.png b/.github/screenshots/4MtAe7Wkev.png new file mode 100644 index 0000000..d352b3d --- /dev/null +++ b/.github/screenshots/4MtAe7Wkev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:770f13947f6cc6e349b68db524b6219798688c8143718fe159205eeca0983410 +size 586736 diff --git a/.github/screenshots/6wz3gW8c2h.png b/.github/screenshots/6wz3gW8c2h.png new file mode 100644 index 0000000..6ebc8de --- /dev/null +++ b/.github/screenshots/6wz3gW8c2h.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06131801cba199fa267b532a2f26b841d2b856cdd1bb9428e08879981e7b36c8 +size 1932256 diff --git a/.github/screenshots/7s0842oAC9.png b/.github/screenshots/7s0842oAC9.png deleted file mode 100644 index 4c55f38..0000000 --- a/.github/screenshots/7s0842oAC9.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:df932e6c24b3d70cb7ad7d7ec3ea240341e4be80cee328eee41d35c36e1937c6 -size 1556410 diff --git a/.github/screenshots/8jipsHiLST.png b/.github/screenshots/8jipsHiLST.png deleted file mode 100644 index 7abc387..0000000 --- a/.github/screenshots/8jipsHiLST.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e2db97ea3b385ac01fb6ec4ac89197dfd4fc6c727639c9493336ddd23be60c7 -size 1003321 diff --git a/.github/screenshots/CpBLzTNM6N.png b/.github/screenshots/CpBLzTNM6N.png new file mode 100644 index 0000000..e661830 --- /dev/null +++ b/.github/screenshots/CpBLzTNM6N.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9864e3e0982f73f59ed32365eec96ff84ec23906bd73fbc23ef8742235c9da4e +size 1734885 diff --git a/.github/screenshots/EWPHmIBEE5.png b/.github/screenshots/EWPHmIBEE5.png index 6fd9f40..90e6416 100644 --- a/.github/screenshots/EWPHmIBEE5.png +++ b/.github/screenshots/EWPHmIBEE5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a03b0cd56b78f51f8bc4c304b8b14fa611748b9efa192ef26283e732beff90c1 -size 643600 +oid sha256:0eeee2b3d31fbb4ea49bb38a2634088fa0a54d6ce5d41f7bf39f419d518b802e +size 850435 diff --git a/.github/screenshots/FHMzJjGOs6.png b/.github/screenshots/FHMzJjGOs6.png deleted file mode 100644 index d9dbbf8..0000000 --- a/.github/screenshots/FHMzJjGOs6.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96eef8c330ed6739ed2a595fa2170c8f5b5cb7a9cec9b4ad58af51a63eb22b4f -size 1325789 diff --git a/.github/screenshots/GL7SkQbHIY.png b/.github/screenshots/GL7SkQbHIY.png new file mode 100644 index 0000000..28544a3 --- /dev/null +++ b/.github/screenshots/GL7SkQbHIY.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a22580330264a0ad2d4a6f758ad26c18ed9a0a17cbe1254dbbad01e959b205f8 +size 110988 diff --git a/.github/screenshots/J5BHVZBh7k.png b/.github/screenshots/J5BHVZBh7k.png deleted file mode 100644 index dc1bf97..0000000 --- a/.github/screenshots/J5BHVZBh7k.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c92749fe4122c719f09a9397345f1a06df4aedee8f50855601be3ec25e85ad3c -size 50175 diff --git a/.github/screenshots/MMeJxl4IXr.png b/.github/screenshots/MMeJxl4IXr.png new file mode 100644 index 0000000..f8cd130 --- /dev/null +++ b/.github/screenshots/MMeJxl4IXr.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ba59fccc691e8f43d4eff532d88edfab411d9abb68d0566fad1c167b7b17bdd +size 183846 diff --git a/.github/screenshots/Pkazk0RufB.png b/.github/screenshots/Pkazk0RufB.png new file mode 100644 index 0000000..3025b0d --- /dev/null +++ b/.github/screenshots/Pkazk0RufB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9db331ad2d2cf2fb2525560baf5970f8faa6c8ff6ae885d9eedd65560af8fffd +size 2035855 diff --git a/.github/screenshots/iunZbvYEGp-ezgif.com-optimize.gif b/.github/screenshots/iunZbvYEGp-ezgif.com-optimize.gif new file mode 100644 index 0000000..bd842a9 --- /dev/null +++ b/.github/screenshots/iunZbvYEGp-ezgif.com-optimize.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22e018fb97f05c24294fd3b8fe088ca755760b81d3b46dd9f04b5f52f98f34da +size 2693240 diff --git a/.github/screenshots/mockup-1777308293568.png b/.github/screenshots/mockup-1777308293568.png new file mode 100644 index 0000000..93558db --- /dev/null +++ b/.github/screenshots/mockup-1777308293568.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e478586834f41ec41e85ab76bec7ca244c2521446a40442ba3de0ea97888fa17 +size 2007401 diff --git a/.github/screenshots/rBY2mgTLy0.png b/.github/screenshots/rBY2mgTLy0.png new file mode 100644 index 0000000..653ddab --- /dev/null +++ b/.github/screenshots/rBY2mgTLy0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eca786ab7f525bcfe663f82995fc9e91234ddd5df726528b0926eaf32ed8ad8e +size 110966 diff --git a/.github/screenshots/xNj7scPEDQ.png b/.github/screenshots/xNj7scPEDQ.png new file mode 100644 index 0000000..4813a47 --- /dev/null +++ b/.github/screenshots/xNj7scPEDQ.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c26e4b9c7f690c49f9625ea2b8c2a82f03a24a0236c53dc02c158f7222c2519d +size 1805877 diff --git a/.github/screenshots/yObFD2LySH.jpg b/.github/screenshots/yObFD2LySH.jpg new file mode 100644 index 0000000..f540a83 --- /dev/null +++ b/.github/screenshots/yObFD2LySH.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f813f98ae41c6d383dcc5e9d6ea693b6701dddc5a03f73cbd1ed990b8532710 +size 1274712 diff --git a/.github/screenshots/zEQxtzhPGx.png b/.github/screenshots/zEQxtzhPGx.png new file mode 100644 index 0000000..1b18477 --- /dev/null +++ b/.github/screenshots/zEQxtzhPGx.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e3bf4df252866dd15f3c1c6efcfc648fbd9d320e47cf2d64551137497d229d6 +size 1324186 diff --git a/.github/screenshots/zl8Dj4xnEw.png b/.github/screenshots/zl8Dj4xnEw.png new file mode 100644 index 0000000..8888187 --- /dev/null +++ b/.github/screenshots/zl8Dj4xnEw.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b68cd855b9e63d3219efe889a129843187639f6fc9c8a27638c983c5740ac9a1 +size 501858 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84ec3e5..e6ff5ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,7 +83,7 @@ jobs: with: type: "zip" directory: ${{ github.workspace }} - filename: "Gameflow-Windows.zip" + filename: "Gameflow-win32-x64.zip" path: "canary-build-Windows" - name: Publish Release @@ -96,4 +96,4 @@ jobs: omitBodyDuringUpdate: true replacesArtifacts: true token: ${{ secrets.GITHUB_TOKEN }} - artifacts: "${{ github.workspace }}/canary-build-*/*.AppImage,${{ github.workspace }}/Gameflow-Windows.zip" + artifacts: "${{ github.workspace }}/canary-build-*/*.AppImage,${{ github.workspace }}/Gameflow-*.zip" diff --git a/.gitignore b/.gitignore index 5d2754a..880e27d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,10 @@ trace downloads .flatpak-builder gameflow-deck.code-workspace -.env.local \ No newline at end of file +.env.local +src/tests/mock-roms/db.sqlite +src/tests/mock-roms/store +src/tests/mock-config +bin +.config/flatpak/repo +xenia.log \ No newline at end of file diff --git a/.versionrc b/.versionrc new file mode 100644 index 0000000..fa5c461 --- /dev/null +++ b/.versionrc @@ -0,0 +1,18 @@ +{ + "packageFiles": [ + { + "filename": "package.json", + "type": "json" + } + ], + "bumpFiles": [ + { + "filename": "package.json", + "type": "json" + }, + { + "filename": "src/packages/gameflow-sdk/package.json", + "type": "json" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 8c7e283..b63a222 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,17 +6,22 @@ "files.watcherExclude": { "**/*.gen.*": true, "src/mainview/gen/*": true, + "**/build": true, + "**/.config/flatpack/repo/**": true, + "**/.flatpak-builder/**": true, }, "search.exclude": { "**/*.gen.*": true, - ".flatpak-builder/**/*": true, + "**/.flatpak-builder": true, + "**/.config/flatpack/repo/**": true, + "**/build": true, "src/mainview/gen/*": true, }, "npm.scriptRunner": "bun", "npm.exclude": [ "**/.flatpak-builder/**/*", "**/build/flatpack/**", - "**/flatpack/repo/**", + "**/.config/flatpack/repo/**", ], "editor.formatOnSave": true, "[typescriptreact]": { @@ -30,9 +35,11 @@ "cSpell.words": [ "elysia", "elysiajs", + "emulatorjs", "gameflow", "hackolade", "keytar", + "mainview", "norigin", "noriginmedia", "romm" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bea05da..e65bd08 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -38,11 +38,6 @@ "label": "Start Dev (Hot Reload)", "type": "shell", "command": "bun run dev:hmr", - "options": { - "env": { - "FORCE_BROWSER": "false" - } - }, "isBackground": true, "problemMatcher": [], "presentation": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 266ed42..0a70168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,84 @@ # Changelog -All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. + +## [1.6.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.5.0...v1.6.0) (2026-05-09) + + +### Features + +* Implemented public plugin system accessible from the store. ([38cb752](https://github.com/simeonradivoev/gameflow-deck/commit/38cb7525527b5ad4f6eb284cdad0001fd87eaf7e)) + +## [1.5.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.4.0...v1.5.0) (2026-05-05) + + +### Features + +* Implemented local game import (with a wizard) ([06b7e40](https://github.com/simeonradivoev/gameflow-deck/commit/06b7e4074da23afdec3b2ff97f84a9e1486944d2)) + + +### Bug Fixes + +* Navigation blocking now working with focuesed input fields ([4da717c](https://github.com/simeonradivoev/gameflow-deck/commit/4da717c26d9840febd48ee87a6a493a3e1acc6b9)) + +## [1.4.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.3.0...v1.4.0) (2026-04-26) + + +### Features + +* Added more ways to detect duplicates ([05fafce](https://github.com/simeonradivoev/gameflow-deck/commit/05fafced07c853deb656d7c17d05184c42ee507c)) +* added update notes and moved update to own tab ([cf84f40](https://github.com/simeonradivoev/gameflow-deck/commit/cf84f40a174b8f242ca58fb6fe02eefab46ff442)) +* Added way to update the local games from romm when IDs change based on IGDB or Retro Achievement ID ([4806f34](https://github.com/simeonradivoev/gameflow-deck/commit/4806f3487a577ab8e7c66907e5b640d95ab8a46c)), closes [#2](https://github.com/simeonradivoev/gameflow-deck/issues/2) +* Bundled NW.js with appimages ([813785f](https://github.com/simeonradivoev/gameflow-deck/commit/813785f4f3d292a87cc4a6b86dc152c43572d2c8)) +* Implemented audio effects ([edbc390](https://github.com/simeonradivoev/gameflow-deck/commit/edbc390d144bf44da35d0f5383ec36eb25c34d1b)) +* Implemented dolphin integration ([a69147a](https://github.com/simeonradivoev/gameflow-deck/commit/a69147a4f73cf626b92622a8ee22b54f538d41a9)) +* Implemented emulator launching ([09b8b9c](https://github.com/simeonradivoev/gameflow-deck/commit/09b8b9c6f850cea3b897308925faf9be02cefa1a)), closes [#1](https://github.com/simeonradivoev/gameflow-deck/issues/1) +* Implemented emulator versions and updating ([34db717](https://github.com/simeonradivoev/gameflow-deck/commit/34db717ec5cbcf8b1ae54fbda33bf9a78f01bd17)) +* Implemented filtering and searching ([444d8c4](https://github.com/simeonradivoev/gameflow-deck/commit/444d8c4c278c6032b37f44a884cb6d7bf0b54c85)) +* implemented haptics ([54dd925](https://github.com/simeonradivoev/gameflow-deck/commit/54dd9256e361877d0950a84061d9402616706352)) +* Implemented romm saves for dolphin and xenia ([7948bd2](https://github.com/simeonradivoev/gameflow-deck/commit/7948bd24fabfc01b7be358f06fcd58c8795826c7)) + + +### Bug Fixes + +* Fixed a bunch of issues on linux ([6aacec2](https://github.com/simeonradivoev/gameflow-deck/commit/6aacec2c0de253a71599e261e07aff53055cdb1e)) +* Fixed emulator details buttons not showing ([04d5856](https://github.com/simeonradivoev/gameflow-deck/commit/04d5856f7d71c944c82877d2a1457facea4b6d31)) +* Fixed tests ([c09fbd3](https://github.com/simeonradivoev/gameflow-deck/commit/c09fbd3dc88891227eda2b9f3bd9ac45621c00ea)) +* logins now refresh on plugins load ([7bd0ebd](https://github.com/simeonradivoev/gameflow-deck/commit/7bd0ebdcca1843076911547ec1098cbaae9e2414)) +* Made self update work on windows ([ae196e1](https://github.com/simeonradivoev/gameflow-deck/commit/ae196e11d616b9813dba11f64e7c844077686db8)) +* Made store downloads extract in their own folder ([764691f](https://github.com/simeonradivoev/gameflow-deck/commit/764691fc8610fafebc93a69ca24f74bcac42a898)) + +## [1.3.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.2.1...v1.3.0) (2026-03-31) + + +### Features + +* Implemented emulator installation ([3750e9e](https://github.com/simeonradivoev/gameflow-deck/commit/3750e9ed8fc1c0919aade9e45a0189838f12b16d)) +* moved to npm package for the store ([91ee719](https://github.com/simeonradivoev/gameflow-deck/commit/91ee7196332313518324cf7195f64d0e92b2cc8b)) + + +### Bug Fixes + +* Added keyboard focus shortcut ([b4e9112](https://github.com/simeonradivoev/gameflow-deck/commit/b4e911298935483bec7e315d2eebee47562bd448)) +* ditched sdl and moved to xinput for windows for less ram usage ([dc0f2d1](https://github.com/simeonradivoev/gameflow-deck/commit/dc0f2d150a37bebefa76988f98d8766f530f44b4)) +* Fixed browser referencing main and getting called twice when in dev mode ([7c10f4e](https://github.com/simeonradivoev/gameflow-deck/commit/7c10f4e4c2b4996e784be051132233a854270250)) +* Fixed romm login, now uses token ([816d50a](https://github.com/simeonradivoev/gameflow-deck/commit/816d50ae4d61723e67a0980ca310561ead661a68)) +* Issues with launching and installation on the steam deck ([ccc5a05](https://github.com/simeonradivoev/gameflow-deck/commit/ccc5a05ed7010adea77eea9190f3149b67702b39)) +* Manual checking for system info to fix bug in library ([a7eb655](https://github.com/simeonradivoev/gameflow-deck/commit/a7eb655a48c6976baa18bb4cde96c989ce8cd375)) +* missing gitlab as download type ([bb8f716](https://github.com/simeonradivoev/gameflow-deck/commit/bb8f7162018f7a320be76128d09da82ccac1a896)) +* switched to node-7z ([90d6711](https://github.com/simeonradivoev/gameflow-deck/commit/90d67119355baa64bd992c9d4e9d11036706bbc9)) + +### [1.2.1](https://github.com/simeonradivoev/gameflow-deck/compare/v1.2.0...v1.2.1) (2026-03-15) + + +### Bug Fixes + +* Added control for opening emulator js menu on steam deck controller ([f33c928](https://github.com/simeonradivoev/gameflow-deck/commit/f33c928633a06d1f99e1125a984059b9ade3a369)) +* Browser not getting closed on manual exit ([489124a](https://github.com/simeonradivoev/gameflow-deck/commit/489124a4a332a7606fb4b8b82f76929c7909a192)) +* Emulators not launching ([fe80b07](https://github.com/simeonradivoev/gameflow-deck/commit/fe80b074d2e5c6c0b9bd9a667f3378455fb5d97a)) +* Fixed cross platform errors and emulatorjs not opening on linux ([df20979](https://github.com/simeonradivoev/gameflow-deck/commit/df20979afa00bd578922a6a516b28845a4b5cab3)) +* minor UI issues ([8125c86](https://github.com/simeonradivoev/gameflow-deck/commit/8125c8695cc84358afdfb2657cc6a3638ae68d69)) +* Wrong webview library path for appimage building ([258ce63](https://github.com/simeonradivoev/gameflow-deck/commit/258ce63bc3cb24c6fb273fd98a1323ae7fde439d)) ## [1.2.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.1.0...v1.2.0) (2026-03-14) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index e400611..0e5864e 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,81 @@ # Gameflow Deck -A Cross-Platform Retro gaming frontend designed for handheld and controllers. -Focused on building a simple user experience and intuitive UI. +A Cross-Platform open source Retro gaming frontend designed for handheld and controllers. +Focused on building a simple user experience and intuitive UI as a curated community driven experience. > [!WARNING] -> This app is actively in development, it doesn't have most of its critical features implemented yet. +> This app is actively in development, it is constantly changing and improving. > It will have an opinionated design and will be used as an experiment in discovering a good UX. +## Community + +Join us on Discord, where you can ask questions, submit ideas and get help. + +[![](https://invidget.switchblade.xyz/R9KakhY67d)](https://discord.gg/R9KakhY67d) + ## Features -- **Cross Platform**: Can run on multiple platforms. Built with web technologies and bun backend. -- **[Romm](https://github.com/rommapp/romm) Support**: Has integration with romm. -- **Lightweight**: It uses the existing system browser to launch the front end, so no need to include a whole web browser. +### Integrations + +- **[ROMM](https://github.com/rommapp/romm)** - download, sync and update roms and platforms. + - Show Achievements and sync playtime. + - Experimental save syncing +- **[Emulator JS](https://github.com/EmulatorJS/EmulatorJS)** - play your games with emulator js right within the app. Uses RetroArch cores. +- **[RClone](https://github.com/rclone/rclone)** - sync saves between devices or cloud. Some Emulators and store games support it. +- **[UMU](https://github.com/Open-Wine-Components/umu-launcher)** - UMU Launcher for playing windows games on linux without needing steam. (Only used for store games for now) + +### Store + +- **Emulators** - (WIP) Download and install emulators and automatically configure them from a list of supported in the store. Some even come with advanced features like cloud saves. +- **Free Curated Games** - Download free curated games and homebrew roms without ever leaving the app + +### Others + +- **Cross Platform** - Can run on multiple platforms. Built with web technologies and bun backend. +- **Steam Deck Support** - Extensively tested with the steam deck. It can use flatpak installed browsers. +- **Lightweight** - It uses the window's webview as a frontend, reducing build size and ram usage. - On Windows it first uses webview2 then your browser - - On linux it uses WebKitGTK or a browser even from flatpak + - On linux it does ship with NW.js to work on most distros. A big one is the steam deck missing WebKitGTK. - Not tested on Mac yet -- **Steam Deck Support**: Extensively tested with the steam deck. It can use flatpak installed browsers. - - Automatic Keyboard prompts -- **Great for Controllers**: The UI is inspired by the switch and works great with joysticks and dpads. -- **Automatic Download** Downloads roms from ROMM automatically -- **Automatic Emulator Discovery** Using the configs of the excellent ES-DE to discover installed emulators and launch games. +- **Great for Controllers** - The UI is inspired by the switch and works great with joysticks and dpads. +- **Automatic Downloads** - Downloads roms from ROMM automatically +- **Automatic Emulator Discovery** - Using the configs of the excellent ES-DE to discover installed emulators and launch roms. You can bring your existing configurations. - Easy fallback configuration with built in file browser. -- **Responsive Layout** Optimized mainly for the steam deck with responsive layout support and dynamic switching of inputs. +- **Responsive Layout** - Optimized mainly for the steam deck with responsive layout support and dynamic switching of inputs. +- **Cloud/Device Save Sync** - For supported games and emulators. +- **Dark and Light** - Dark and light themes for your preference. ## Screenshots - - - - - + + + + + + + + + + ## Goals -I want to build an open and free platform where you can play and discover new hidden gems from the past. -I plan to add a free store where you can download all your needed emulators, the goal is to not have to leave the UI for anything. -I really want to add matrix chat support in the app for engaging with your favorite community. Having access to so many nodejs libraries would make it quite straight forward. +- I want to build an open and free platform where you can play and discover new hidden gems from the past. +- I plan to add a free store where you can download all your needed emulators, the goal is to not have to leave the UI for anything. +- I really want to add matrix chat support in the app for engaging with your favorite community. Having access to so many nodejs libraries would make it quite straight forward. +- I'm sick of closed source and private store fronts, and want a way to share community curated free experiences. I'm also sick of the profit driven nature of games and promotions. +- Being self contained, I want to avoid writing as little as possible to system and contain and manage settings in a custom changeable directory. This was mainly a side-effect of having the low storage steam deck and always running out of space on my internal hard drive. + +## Usage + +There are currently 2 ways of getting games. One is logging in through romm and importing your games from there. The other is the store (it's a bit limited right now). I might add local import of roms since IGDB login is already implemented. + +The app created a default folder in your home folder. You can move it. It stores everything there. From downloaded roms, emulators and configs. + +## Existing Setups + +The game should work pretty well with existing emulators one has installed. It uses the ES-DE config to find installed emulators. Only downside is more advanced integrations won't work, as they are mainly used for store emulators where the app has more control over, plus I don't want to mess up existing setups. +But given it's an existing setup, say from emudeck it won't matter much as it's already configured say for the steam deck. ## Development @@ -66,6 +106,17 @@ I really want to add matrix chat support in the app for engaging with your favor - `bun run openapi-ts` generated the openapi client calls from romm's API - `bun run package:windows` builds an package to be distributed on windows - `bun run package:linux` builds an AppImage to be distributed on linux + - `bun run test` run tests + - `bun run download:chromium` downloads degoogled chromium to use as the frontend + - `bun run download:nwjs` downloads NW.js to use as a frontend. + +## Plugins + +To create a plugin create a new npm project and install: +`bun i --peer @simeonradivoev/gameflow-sdk` + +Then publish the package to npmjs with a tag `gameflow-plugin` to appear in the UI. +For more info check the [SDK README](./scripts/sdk/README.md) ### Tech Stack @@ -77,3 +128,11 @@ I really want to add matrix chat support in the app for engaging with your favor - [Tanstack](https://tanstack.com/) router and query for navigation and data - [elysia](https://elysiajs.com/) for the APIs - [webview](https://github.com/webview/webview) for launching existing system webviews instead of full browser if possible. +- [emulatorjs](https://emulatorjs.org/) for playing lots of roms inside the app without having to deal with external emulators + +### Credits + +- UI Sounds + - [CC BY 4.0 - Credit: JC Sounds](https://opengameart.org/content/jc-sounds-ui-utility-pack-vol-1) + - [Sounds by: Chhoff](https://chhoffmusic.itch.io/classic-ui-sfx) + - [UI Sound Effects by lolurio](https://lolurio.itch.io/lolurios-free-cozy-ui-sfx) diff --git a/bun.lock b/bun.lock index 9bf51a2..f005808 100644 --- a/bun.lock +++ b/bun.lock @@ -5,97 +5,155 @@ "": { "name": "electrobun-hello-world", "dependencies": { + "7zip-bin": "^5.2.0", "@auth/core": "^0.34.3", - "@elysiajs/cors": "^1.4.1", - "@elysiajs/eden": "^1.4.6", - "@elysiajs/static": "^1.4.7", - "@jimp/wasm-webp": "^1.6.0", + "@elysiajs/cors": "^1.4.2", + "@elysiajs/eden": "^1.4.9", + "@jimp/wasm-webp": "^1.6.1", + "@phalcode/ts-igdb-client": "^1.0.26", "cheerio": "^1.2.0", - "conf": "^15.0.2", - "drizzle-orm": "^0.45.1", - "elysia": "^1.4.22", - "fs-extra": "^11.3.3", + "conf": "^15.1.0", + "drizzle-orm": "^0.45.2", + "elysia": "^1.4.28", + "fs-extra": "^11.3.5", "get-folder-size": "^5.0.0", - "jimp": "^1.6.0", + "ini": "^6.0.0", + "jimp": "^1.6.1", + "mustache": "^4.2.0", + "node-7z": "^3.0.0", "node-disk-info": "^1.3.0", - "node-downloader-helper": "^2.1.10", + "node-downloader-helper": "^2.1.11", "node-stream-zip": "^1.15.0", + "node-unrar-js": "^2.0.2", "open": "^11.0.0", + "p-queue": "^9.2.0", "pathe": "^2.0.3", - "systeminformation": "^5.31.1", - "tough-cookie": "^6.0.0", + "slugify": "^1.6.9", + "smol-toml": "^1.6.1", + "systeminformation": "^5.31.6", + "tapable": "^2.3.3", + "tough-cookie": "^6.0.1", "tough-cookie-file-store": "^3.3.0", - "ts-igdb-client": "^0.4.2", "unzip-stream": "^0.3.4", "webview-bun": "^2.4.0", - "zod": "^4.3.6", + "zod": "^4.4.3", }, "devDependencies": { - "@ap0nia/eden": "^1.0.0-next.22", + "@ap0nia/eden": "^1.6.1", "@ap0nia/eden-tanstack-query": "^1.0.0-next.22", "@emulatorjs/emulatorjs": "^4.2.3", - "@hey-api/openapi-ts": "^0.91.0", - "@noriginmedia/norigin-spatial-navigation": "^2.3.0", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-form": "^1.28.0", - "@tanstack/react-query": "^5.90.20", - "@tanstack/react-query-devtools": "^5.91.3", - "@tanstack/react-router": "^1.157.16", - "@tanstack/react-router-devtools": "^1.154.12", - "@tanstack/react-router-ssr-query": "^1.157.17", - "@tanstack/router-plugin": "^1.157.16", - "@tanstack/zod-adapter": "^1.162.4", + "@hey-api/openapi-ts": "^0.91.1", + "@noriginmedia/norigin-spatial-navigation": "^3.1.0", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-form": "^1.32.0", + "@tanstack/react-query": "^5.100.10", + "@tanstack/react-query-devtools": "^5.100.10", + "@tanstack/react-query-persist-client": "^5.100.10", + "@tanstack/react-router": "^1.169.2", + "@tanstack/react-router-devtools": "^1.166.13", + "@tanstack/react-router-ssr-query": "^1.166.12", + "@tanstack/router-plugin": "^1.167.35", + "@tanstack/zod-adapter": "^1.166.9", + "@types/adm-zip": "^0.5.8", + "@types/audiosprite": "^0.7.3", "@types/bun": "latest", "@types/fs-extra": "^11.0.4", - "@types/react": "^19.2.9", + "@types/howler": "^2.2.12", + "@types/ini": "^4.1.1", + "@types/json-schema": "^7.0.15", + "@types/mustache": "^4.2.6", + "@types/node-7z": "^2.1.11", + "@types/rclone.js": "^0.6.3", + "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/unzip-stream": "^0.3.4", - "@vitejs/plugin-react": "^5.1.2", + "@vitejs/plugin-react": "^5.2.0", + "adm-zip": "^0.5.17", "animate.css": "^4.1.1", "app-builder-bin": "^5.0.0-alpha.13", + "audiosprite": "^0.7.2", "babel-plugin-react-compiler": "^1.0.0", "classnames": "^2.5.1", + "commit-and-tag-version": "^12.7.3", "concurrently": "^9.2.1", "cross-env": "^10.1.0", - "daisyui": "^5.5.14", - "drizzle-kit": "^0.31.9", - "dts-bundle-generator": "^9.5.1", + "daisyui": "^5.5.19", + "drizzle-kit": "^0.31.10", "eden-tanstack-query": "^0.0.9", + "howler": "^2.2.4", + "idb-keyval": "^6.2.2", "lucide-react": "^0.563.0", "pretty-bytes": "^7.1.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-error-boundary": "^6.1.0", + "pretty-ms": "^9.3.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-error-boundary": "^6.1.1", "react-hot-toast": "^2.6.0", - "react-qr-code": "^2.0.18", - "sass-embedded": "^1.97.3", - "standard-version": "^9.5.0", - "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.18", + "react-markdown": "^10.1.0", + "react-qr-code": "^2.0.21", + "sass-embedded": "^1.99.0", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "usehooks-ts": "^3.1.1", - "vite": "^7.3.1", - "vite-plugin-svg-icons-ng": "^1.5.2", + "vite": "^7.3.3", + "vite-plugin-svg-icons-ng": "^1.9.1", "vite-static-assets-plugin": "^1.2.2", "vite-tsconfig-paths": "^6.1.1", }, }, + "src/packages/gameflow-sdk": { + "name": "@simeonradivoev/gameflow-sdk", + "version": "1.6.0", + "bin": { + "gameflow-build": "build.ts", + }, + "peerDependencies": { + "7zip-bin": "^5.2.0", + "@auth/core": "^0.34.3", + "cheerio": "^1.2.0", + "conf": "^15.1.0", + "drizzle-orm": "^0.45.2", + "elysia": "^1.4.28", + "fs-extra": "^11.3.5", + "get-folder-size": "^5.0.0", + "ini": "^6.0.0", + "jimp": "^1.6.1", + "mustache": "^4.2.0", + "node-7z": "^3.0.0", + "node-disk-info": "^1.3.0", + "node-downloader-helper": "^2.1.11", + "node-stream-zip": "^1.15.0", + "node-unrar-js": "^2.0.2", + "open": "^11.0.0", + "p-queue": "^9.2.0", + "pathe": "^2.0.3", + "slugify": "^1.6.9", + "smol-toml": "^1.6.1", + "tapable": "^2.3.3", + "unzip-stream": "^0.3.4", + "zod": "^4.4.3", + }, + }, }, "packages": { - "@ap0nia/eden": ["@ap0nia/eden@1.0.0-next.22", "", { "peerDependencies": { "elysia": "^1.3.1" } }, "sha512-9iH09koK29Yuem80fz8nCt9iHVcJqxUo2QHAr4psI02PhvL70n6aWVo/hlHyYXwOSsSgRQlLl1vPmiulFOUFoA=="], + "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + + "@ap0nia/eden": ["@ap0nia/eden@1.6.1", "", { "dependencies": { "elysia": "1.2.15" } }, "sha512-jlsUyh4PsYNnMcPuQ3IJq0hhDNnyRNGYx+MSAJlcgKs4En9qrokLorSbTRvVjA1Mdx4VdzEADcPn99Kbph0SOw=="], "@ap0nia/eden-tanstack-query": ["@ap0nia/eden-tanstack-query@1.0.0-next.22", "", {}, "sha512-eSQ98G4TYzrAdsfRekrvqIrTqrAUFy+YpibZ5fj5KL6/R6FcrS2U2F51iML98baXT4MTpOJARY9p+7x0hiA8Qw=="], "@auth/core": ["@auth/core@0.34.3", "", { "dependencies": { "@panva/hkdf": "^1.1.1", "@types/cookie": "0.6.0", "cookie": "0.6.0", "jose": "^5.1.3", "oauth4webapi": "^2.10.4", "preact": "10.11.3", "preact-render-to-string": "5.2.3" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^7" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-jMjY/S0doZnWYNV90x0jmU3B+UcrsfGYnukxYrRbj0CVvGI/MX3JbHsxSrx2d4mbnXaUsqJmAcDfoQWA6r0lOw=="], - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], - "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], @@ -115,7 +173,7 @@ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], @@ -137,11 +195,9 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="], + "@elysiajs/cors": ["@elysiajs/cors@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-FTCcbH35brTLigF1W7BYySRZomgI/dBEMK9BgK9RP9Nez7zmpGh4koL/Yr1BFv8nYz7CfhRvcM8d/c+XnwMaVQ=="], - "@elysiajs/eden": ["@elysiajs/eden@1.4.6", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q=="], - - "@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="], + "@elysiajs/eden": ["@elysiajs/eden@1.4.9", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-3CKVD4ycVjB8nCNssfmhnUuq3SzSHkUES3v5PNCFr9LxIrx39/HVRAZ8z2sLxrFqzUs48dCBZaxoZzJ5UUVHDA=="], "@emulatorjs/core-81": ["@emulatorjs/core-81@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-oPQEqjpR3z7Yedte4u3sOXDZ4NXAykNcbENjYcB+x3QshF8I+3MQCo8kINOT2lsqqgx91WR4kmEaYQqU39YsDA=="], @@ -301,13 +357,13 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@hey-api/codegen-core": ["@hey-api/codegen-core@0.6.0", "", { "dependencies": { "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-O6TRP3g1188gdpZC9yio64KkvWD97QpLjaCFnwTgykTZtzQXZbkyw+lYPVfZ7Ee+m7eLau1/jEJmQcUY9G0sLw=="], + "@hey-api/codegen-core": ["@hey-api/codegen-core@0.6.1", "", { "dependencies": { "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-khTIpxhKEAqmRmeLUnAFJQs4Sbg9RPokovJk9rRcC8B5MWH1j3/BRSqfpAIiJUBDU1+nbVg2RVCV+eQ174cdvw=="], "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-gRbyyTjzpFVNmbD+Upn3w4dWV+bCXGJbff3A7leDO/tfNxSz1xIb6Ad/5UKtvEW9kDt/2Uyc3XkFZ6rpafvbfQ=="], - "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.91.0", "", { "dependencies": { "@hey-api/codegen-core": "0.6.0", "@hey-api/json-schema-ref-parser": "1.2.3", "@hey-api/shared": "0.1.0", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-AHkd982HsPz1XpqRm59URwJyJqTzyzzC30EAp07b/0M9KojjneCPxm8FnvFnXLRTMyKgcOymMsYXuLzJ9mpMHA=="], + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.91.1", "", { "dependencies": { "@hey-api/codegen-core": "0.6.1", "@hey-api/json-schema-ref-parser": "1.2.3", "@hey-api/shared": "0.1.1", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-d16WR35UtthK/ihAIwJaKxrj/zvb5LbYwtVJCyZFFMin2qzDU8Y3Lpk78ensAykrLoaDLzpd0iIyt9JCP5Qmww=="], - "@hey-api/shared": ["@hey-api/shared@0.1.0", "", { "dependencies": { "@hey-api/codegen-core": "0.6.0", "@hey-api/json-schema-ref-parser": "1.2.3", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-qEDMSBWEEWxcBU5XHacjCCnFOVq1YWPPR3owURVep60I7ejfSG5OINxM4eF+p3KJGMcZduzzfq9pd1grStHZBg=="], + "@hey-api/shared": ["@hey-api/shared@0.1.1", "", { "dependencies": { "@hey-api/codegen-core": "0.6.1", "@hey-api/json-schema-ref-parser": "1.2.3", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-/irgNGXw9TL5aKB3S7jCLgh07vgDFkYjSjz7vEWO9xEe6MUhx76zSFzkPspk2UrLghYayvmaKPf1ky4XjNI9ZQ=="], "@hey-api/types": ["@hey-api/types@0.1.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw=="], @@ -319,63 +375,63 @@ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], + "@jimp/core": ["@jimp/core@1.6.1", "", { "dependencies": { "@jimp/file-ops": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^21.3.3", "mime": "3" } }, "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A=="], - "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], + "@jimp/diff": ["@jimp/diff@1.6.1", "", { "dependencies": { "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" } }, "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ=="], - "@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="], + "@jimp/file-ops": ["@jimp/file-ops@1.6.1", "", {}, "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w=="], - "@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="], + "@jimp/js-bmp": ["@jimp/js-bmp@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "bmp-ts": "^1.0.9" } }, "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ=="], - "@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="], + "@jimp/js-gif": ["@jimp/js-gif@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w=="], - "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="], + "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "jpeg-js": "^0.4.4" } }, "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg=="], - "@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="], + "@jimp/js-png": ["@jimp/js-png@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "pngjs": "^7.0.0" } }, "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA=="], - "@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="], + "@jimp/js-tiff": ["@jimp/js-tiff@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "utif2": "^4.1.0" } }, "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw=="], - "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="], + "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ=="], - "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="], + "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/utils": "1.6.1" } }, "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ=="], - "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="], + "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ=="], - "@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="], + "@jimp/plugin-color": ["@jimp/plugin-color@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A=="], - "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="], + "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg=="], - "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="], + "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg=="], - "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="], + "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg=="], - "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="], + "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g=="], - "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="], + "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1" } }, "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew=="], - "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="], + "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA=="], - "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="], + "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA=="], - "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="], + "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/js-bmp": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/js-tiff": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "any-base": "^1.1.0" } }, "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A=="], - "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="], + "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A=="], - "@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="], + "@jimp/plugin-print": ["@jimp/plugin-print@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/types": "1.6.1", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w=="], - "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="], + "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.1", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww=="], - "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="], + "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg=="], - "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="], + "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg=="], - "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="], + "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-hash": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q=="], - "@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="], + "@jimp/types": ["@jimp/types@1.6.1", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w=="], - "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], + "@jimp/utils": ["@jimp/utils@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "tinycolor2": "^1.6.0" } }, "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw=="], - "@jimp/wasm-webp": ["@jimp/wasm-webp@1.6.0", "", { "dependencies": { "@jsquash/webp": "^1.4.0", "zod": "^3.23.8" } }, "sha512-P0zUpK6n2XIAn8bt0F6rhSn1+FgteBTrL+TBb6Oqw8v5qEDJoNYkd6LlfZYN8YwtRBTBdZ8GFnWsg2Sar+qOkA=="], + "@jimp/wasm-webp": ["@jimp/wasm-webp@1.6.1", "", { "dependencies": { "@jsquash/webp": "^1.4.0", "zod": "^3.23.8" } }, "sha512-t+Wqkde4xQHP/UZ4bDiDo3pbhFz32E7FvQCUkuFdJDmEDl6gPCs6LQiQVBmumUQYTeVLiLtLzlM9j8s7yF0sXQ=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -393,6 +449,8 @@ "@jsquash/webp": ["@jsquash/webp@1.5.0", "", { "dependencies": { "wasm-feature-detect": "^1.2.11" } }, "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw=="], + "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], + "@node-minify/clean-css": ["@node-minify/clean-css@9.0.1", "", { "dependencies": { "@node-minify/utils": "9.0.1", "clean-css": "5.3.3" } }, "sha512-GHTMmjGloRvNzqdG7foI0iZeS2QmuYCQvdASJP9sCKjkpH45bygODpXPYKnlzUEpQgYvPK9Q3GxqYnVY9SdoqA=="], "@node-minify/core": ["@node-minify/core@9.0.2", "", { "dependencies": { "@node-minify/utils": "9.0.1", "glob": "10.3.3", "mkdirp": "3.0.1" } }, "sha512-FNhv29Wom6wKrrFKaeAfmZqz7TX5A1E6P+bpd0VIc+DYWMLUIhAViS8riaZg3A1oD0s06s+5BG2Fg7RqMKiKHw=="], @@ -401,13 +459,11 @@ "@node-minify/utils": ["@node-minify/utils@9.0.1", "", { "dependencies": { "gzip-size": "6.0.0" } }, "sha512-aC1+mhKTP3IMa2VcuGl3ui92LO/7CPQWldNGzu3BVGKiMNJ70AKJW/R6huuYCSuQyHDGM9oFwiVClsZnFxn67g=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "@noriginmedia/norigin-spatial-navigation": ["@noriginmedia/norigin-spatial-navigation@3.1.0", "", { "dependencies": { "@noriginmedia/norigin-spatial-navigation-core": "^3.1.0", "@noriginmedia/norigin-spatial-navigation-react": "^3.1.0" } }, "sha512-KPge4ocpDFde7cpZ2aqrPrKmxOxkue983NsfpmE/vX4k2l+Ik8UkucCWGqkcy81TXkEyRhdsYwFTRePNB5qUCg=="], - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + "@noriginmedia/norigin-spatial-navigation-core": ["@noriginmedia/norigin-spatial-navigation-core@3.1.0", "", { "dependencies": { "lodash-es": "^4.17.21" } }, "sha512-AFxJHurTqy+I3NLnaXsLUBa9FZjUryMNFEdLpPrITSqDjk525aINeLMOK1PN7WTiK5xpHL0pbpw0+uVOfWgp4w=="], - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@noriginmedia/norigin-spatial-navigation": ["@noriginmedia/norigin-spatial-navigation@2.3.0", "", { "dependencies": { "lodash": "^4.17.21" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-gR//N45NnKz1h0/AVknkfg7QnNATETdgXUUD3EKPxuQPyhk7NhsphODzRamyvjYaxsU6VbY/szcUlzBWWBkNMw=="], + "@noriginmedia/norigin-spatial-navigation-react": ["@noriginmedia/norigin-spatial-navigation-react@3.1.0", "", { "dependencies": { "@noriginmedia/norigin-spatial-navigation-core": "^3.1.0", "lodash-es": "^4.17.21" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-F2PIqzTnlYbbc+oRdIQfBf7e1VcA1uhyjze4uOal8FHI8tZs1U8nomH84+2KcM6G3EM/XGexgQsPy5f5dtrmUA=="], "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], @@ -439,9 +495,13 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + "@phalcode/ts-apicalypse": ["@phalcode/ts-apicalypse@1.0.26", "", { "dependencies": { "axios": "^1.11.0" } }, "sha512-RdqkuunEYu63hRs4tYZ6FLTC17ynC6AJ/YUppRGSIyr6pm5pI/vB1qlEaeUr/f4JJsNmbFwGjnMJdXvoP1LmWA=="], + + "@phalcode/ts-igdb-client": ["@phalcode/ts-igdb-client@1.0.26", "", { "dependencies": { "@phalcode/ts-apicalypse": "^1.0.26", "axios": "^1.11.0" } }, "sha512-ITBazxhafHDBVJFI6THrLOT8OuO4zhD9pOeKQUFJ80soKhBevvbJz3tzkt24fF783Hoqaja8rWmGSwcN04d5gA=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], @@ -493,89 +553,99 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], + "@simeonradivoev/gameflow-sdk": ["@simeonradivoev/gameflow-sdk@workspace:src/packages/gameflow-sdk"], + "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], - "@tanstack/form-core": ["@tanstack/form-core@1.28.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.7.7" } }, "sha512-MX3YveB6SKHAJ2yUwp+Ca/PCguub8bVEnLcLUbFLwdkSRMkP0lMGdaZl+F0JuEgZw56c6iFoRyfILhS7OQpydA=="], + "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="], - "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="], + "@tanstack/form-core": ["@tanstack/form-core@1.32.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.9.1" } }, "sha512-Tn5VRDSjyqjmaet2tJMuEWDRFyrCaon03vxXPlSSaiSs6C/N7lCIwGCXJbZXEUq1kTj8jYN9qyXHbsz4LQHcow=="], + + "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], "@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.100.10", "", {}, "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w=="], - "@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="], + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.100.10", "", {}, "sha512-3DmJf25hDPus5IpVvp6ujXv6bKV2zPzI9vpbAmpJigsL/H6DPvPjmf7/Q9yVKEke//8fgeQ45abjgnLuyYxAiw=="], - "@tanstack/react-form": ["@tanstack/react-form@1.28.0", "", { "dependencies": { "@tanstack/form-core": "1.28.0", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-ibLcf5QkTogV0Ly944CuqGxWTpHyreNA4Cy8Wtky7zE9wtE3HVapQt4/hUuXo51zihfTkv5URiXpoTSKF5Xosg=="], + "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.100.10", "", { "dependencies": { "@tanstack/query-core": "5.100.10" } }, "sha512-O9Pey40DhTTDBABS0bHr+KNL5/VMf6PrqjexS8WoDDtnkaoWM+y0MSe0V9E5W+BwvkjM33mB3aYcCxa175gZTQ=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="], + "@tanstack/react-form": ["@tanstack/react-form@1.32.0", "", { "dependencies": { "@tanstack/form-core": "1.32.0", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6WP5SQTA6/H9crCpvpq3ZppYWqtrdE5NjOy6ebABi6uAQPqhfTzrdjS9t40mCZCFtGI5585OhJV6zBP/KN2zcw=="], - "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="], + "@tanstack/react-query": ["@tanstack/react-query@5.100.10", "", { "dependencies": { "@tanstack/query-core": "5.100.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q=="], - "@tanstack/react-router": ["@tanstack/react-router@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.157.16", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg=="], + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.10", "", { "dependencies": { "@tanstack/query-devtools": "5.100.10" }, "peerDependencies": { "@tanstack/react-query": "^5.100.10", "react": "^18 || ^19" } }, "sha512-zes0+o9ef5rAZXJ9f/SeaLs2nufJaeVkZkl/Or9NGrWVF41kL9Od9ED9nCwtQlgiF2VGtrzhEw5AU/igAO+aAg=="], - "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.154.12", "", { "dependencies": { "@tanstack/router-devtools-core": "1.154.12" }, "peerDependencies": { "@tanstack/react-router": "^1.154.12", "@tanstack/router-core": "^1.154.12", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-TcGe7pmeVjk1zD58eMR87GG9OXMx6LDGz5QopmJS4LafvK2hvuaht+eKBnZlCvKLPlXu5juwHT4u+2bYdn6sqQ=="], + "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.100.10", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.100.10" }, "peerDependencies": { "@tanstack/react-query": "^5.100.10", "react": "^18 || ^19" } }, "sha512-EImacngLXYEtzlrIPf8IAqKN3foS7cmSj4GWqsHJvc7K+8fy2c3s7mdV8oTJeii/TvrzO4X9fcnXi6tUHMIOHA=="], - "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.157.17", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.157.16" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-IrLi+cNLOAiTShuwGUi9WCNWXMG4993fnyXnWMC53M64rhUngPRFCAQiA05BavE/d7bifC6WKDTt1ZhC1Pewaw=="], + "@tanstack/react-router": ["@tanstack/react-router@1.169.2", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.169.2", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ=="], - "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], + "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.166.13", "", { "dependencies": { "@tanstack/router-devtools-core": "1.167.3" }, "peerDependencies": { "@tanstack/react-router": "^1.168.15", "@tanstack/router-core": "^1.168.11", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA=="], - "@tanstack/router-core": ["@tanstack/router-core@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eJuVgM7KZYTTr4uPorbUzUflmljMVcaX2g6VvhITLnHmg9SBx9RAgtQ1HmT+72mzyIbRSlQ1q0fY/m+of/fosA=="], + "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.166.12", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.168.0" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-yDUIoEh+PimAcWmk/2BE0EkI8TwLVeToNzoIuwahmTtBUR+ptZPWbtiPjudO8JZ0BhT3odHtuOn1eBOK0/4NAQ=="], - "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.154.12", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.154.12", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-lvnP9cqknvSSkUjqQRVn61TcBhq72hCFFOzMwdFdFPTO8nMEXvYE6ZZJiXtivwcvsKmO6XVFLMXuJr/928gNkw=="], + "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.157.16", "", { "dependencies": { "@tanstack/router-core": "1.157.16", "@tanstack/router-utils": "1.154.7", "@tanstack/virtual-file-routes": "1.154.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-Ae2M00VTFjjED7glSCi/mMLENRzhEym6NgjoOx7UVNbCC/rLU/5ASDe5VIlDa8QLEqP5Pj088Gi51gjmRuICvQ=="], + "@tanstack/router-core": ["@tanstack/router-core@1.169.2", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.157.16", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.157.16", "@tanstack/router-generator": "1.157.16", "@tanstack/router-utils": "1.154.7", "@tanstack/virtual-file-routes": "1.154.7", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.157.16", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-YQg7L06xyCJAYyrEJNZGAnDL8oChILU+G/eSDIwEfcWn5iLk+47x1Gcdxr82++47PWmOPhzuTo8edDQXWs7kAA=="], + "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.167.3", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.168.11", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg=="], - "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.157.16", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-YuwNG4jdtn+r90yyti8yP27IKaVoflWmRezqnj0gyJxpRauBkK7MVLvWSNbJadnk88b9H+rdtNOF2k3owGaong=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.166.42", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.169.2", "@tanstack/router-utils": "1.161.8", "@tanstack/virtual-file-routes": "1.161.7", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^3.24.2" } }, "sha512-2qBWC0t78r6b3vI+AbnvCZcFAvbYBDlLuWZrTjQbcjUmwG3qyeQp983tJyDuj9wb5//adG1tgAGXZkJ3aDwdBg=="], - "@tanstack/router-utils": ["@tanstack/router-utils@1.154.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-61bGx32tMKuEpVRseu2sh1KQe8CfB7793Mch/kyQt0EP3tD7X0sXmimCl3truRiDGUtI0CaSoQV1NPjAII1RBA=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.35", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.169.2", "@tanstack/router-generator": "1.166.42", "@tanstack/router-utils": "1.161.8", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^3.0.0", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.169.2", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-UAScU5VAzLYVY4FML/Cbc5S5TucT4I8Ata05yozGOe4ZfepTKRffA5xWLtD2N+ov5svdv0KTX/kqlZnYPe28mA=="], - "@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], + "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.168.0", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-5yBUAF1d9z2kOFKoz1spvpvkMSTmRnRXEwi+bGKfrXYmt7CfHu3Pk8KUFMln67uQoKQ9VTkcd5tLkjJVrZ2/AQ=="], - "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.154.7", "", {}, "sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg=="], + "@tanstack/router-utils": ["@tanstack/router-utils@1.161.8", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-xyiLWEKjfBAVhauDSSjXxyf7s8elU6SM+V050sbkofvGmIIvkwPFtDsX7Gvwh14kBd6iCwAT+RiPvXTxAptY0Q=="], - "@tanstack/zod-adapter": ["@tanstack/zod-adapter@1.162.4", "", { "peerDependencies": { "@tanstack/react-router": ">=1.43.2", "zod": "^3.23.8" } }, "sha512-sO4n2o9F7gZKHZb/nW/fMcDaeVcbFZ2a7zCA+GkaHJwRmhKKlQQ0dae9pc8wOMMG+QkfH1Wysq+tg2RNvm/kpg=="], + "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="], + + "@tanstack/zod-adapter": ["@tanstack/zod-adapter@1.166.9", "", { "peerDependencies": { "@tanstack/react-router": ">=1.43.2", "zod": "^3.23.8" } }, "sha512-HHllQ/CKGi8YBbftv6OmzojtHM6Rk4UszAFICAgUMbwiqtKqjlIZQ/7mv2IPNxBb8YlOQgzyQ4jz2UTEXIi6YA=="], "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="], + "@types/adm-zip": ["@types/adm-zip@0.5.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q=="], + + "@types/audiosprite": ["@types/audiosprite@0.7.3", "", {}, "sha512-P4rUuHPt2kWPMqyObfh1SfqS2H/ZuTxByh00ecuI2tOdvP5b8NznuBeQgemDXV9v8b4pewFPB9G3BuYRONqD7A=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -585,44 +655,68 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/howler": ["@types/howler@2.2.12", "", {}, "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g=="], + + "@types/ini": ["@types/ini@4.1.1", "", {}, "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/mustache": ["@types/mustache@4.2.6", "", {}, "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw=="], + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], + "@types/node-7z": ["@types/node-7z@2.1.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-7gwLx44tqZqjyrvjkX41CWW4h7+aXrazFg/JR6N5g+R5BW1eqsNuw8SNLWrh7KcnfKhAYFiWyNb10ti5v5eCmQ=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - "@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="], + "@types/rclone.js": ["@types/rclone.js@0.6.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-BssKAAVRY//fxGKso8SatyOwiD7X0toDofNnVxZlIXmN7UHrn2UBTxldNAjgUvWA91qJyeEPfKmeJpZVhLugXg=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/unzip-stream": ["@types/unzip-stream@0.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-ud0vtsNRF+joUCyvNMyo0j5DKX2Lh/im+xVgRzBEsfHhQYZ+i4fKTveova9XxLzt6Jl6G0e/0mM4aC0gqZYSnA=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="], + "adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -633,7 +727,7 @@ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], @@ -649,25 +743,25 @@ "arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], - "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "async": ["async@0.9.2", "", {}, "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="], + "audiosprite": ["audiosprite@0.7.2", "", { "dependencies": { "async": "~0.9.0", "glob": "^6.0.4", "mkdirp": "^0.5.0", "optimist": "~0.6.1", "underscore": "~1.8.3", "winston": "~1.0.0" }, "bin": { "audiosprite": "./cli.js" } }, "sha512-9Z6UwUuv4To5nUQNRIw5/Q3qA7HYm0ANzoW5EDGPEsU2oIRVgmIlLlm9YZfpPKoeUxt54vMStl2/762189VmJw=="], + "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], - "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], + "axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], @@ -689,13 +783,11 @@ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="], - "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -711,9 +803,19 @@ "caniuse-lite": ["caniuse-lite@1.0.30001765", "", {}, "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -731,18 +833,24 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], "colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="], + "colors": ["colors@1.0.3", "", {}, "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "commit-and-tag-version": ["commit-and-tag-version@12.7.3", "", { "dependencies": { "chalk": "^2.4.2", "conventional-changelog": "4.0.0", "conventional-changelog-config-spec": "2.1.0", "conventional-changelog-conventionalcommits": "6.1.0", "conventional-recommended-bump": "7.0.1", "detect-indent": "^6.1.0", "detect-newline": "^3.1.0", "dotgitignore": "^2.1.0", "fast-xml-parser": "^5.5.6", "figures": "^3.2.0", "find-up": "^5.0.0", "git-semver-tags": "^5.0.1", "semver": "^7.7.2", "yaml": "^2.6.0", "yargs": "^17.7.2" }, "bin": { "commit-and-tag-version": "bin/cli.js" } }, "sha512-rbauuCDU98yEHMy/LrNNu8HLTuGv7C2kN/3GXC59L18aJGii0eiryCESb1SEHXNFem2/2ngWG/Pq6qaCqw3aCw=="], + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -751,51 +859,51 @@ "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], - "conf": ["conf@15.0.2", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw=="], + "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "conventional-changelog": ["conventional-changelog@3.1.25", "", { "dependencies": { "conventional-changelog-angular": "^5.0.12", "conventional-changelog-atom": "^2.0.8", "conventional-changelog-codemirror": "^2.0.8", "conventional-changelog-conventionalcommits": "^4.5.0", "conventional-changelog-core": "^4.2.1", "conventional-changelog-ember": "^2.0.9", "conventional-changelog-eslint": "^3.0.9", "conventional-changelog-express": "^2.0.6", "conventional-changelog-jquery": "^3.0.11", "conventional-changelog-jshint": "^2.0.9", "conventional-changelog-preset-loader": "^2.3.4" } }, "sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ=="], + "conventional-changelog": ["conventional-changelog@4.0.0", "", { "dependencies": { "conventional-changelog-angular": "^6.0.0", "conventional-changelog-atom": "^3.0.0", "conventional-changelog-codemirror": "^3.0.0", "conventional-changelog-conventionalcommits": "^6.0.0", "conventional-changelog-core": "^5.0.0", "conventional-changelog-ember": "^3.0.0", "conventional-changelog-eslint": "^4.0.0", "conventional-changelog-express": "^3.0.0", "conventional-changelog-jquery": "^4.0.0", "conventional-changelog-jshint": "^3.0.0", "conventional-changelog-preset-loader": "^3.0.0" } }, "sha512-JbZjwE1PzxQCvm+HUTIr+pbSekS8qdOZzMakdFyPtdkEWwFvwEJYONzjgMm0txCb2yBcIcfKDmg8xtCKTdecNQ=="], - "conventional-changelog-angular": ["conventional-changelog-angular@5.0.13", "", { "dependencies": { "compare-func": "^2.0.0", "q": "^1.5.1" } }, "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA=="], + "conventional-changelog-angular": ["conventional-changelog-angular@6.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg=="], - "conventional-changelog-atom": ["conventional-changelog-atom@2.0.8", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw=="], + "conventional-changelog-atom": ["conventional-changelog-atom@3.0.0", "", {}, "sha512-pnN5bWpH+iTUWU3FaYdw5lJmfWeqSyrUkG+wyHBI9tC1dLNnHkbAOg1SzTQ7zBqiFrfo55h40VsGXWMdopwc5g=="], - "conventional-changelog-codemirror": ["conventional-changelog-codemirror@2.0.8", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw=="], + "conventional-changelog-codemirror": ["conventional-changelog-codemirror@3.0.0", "", {}, "sha512-wzchZt9HEaAZrenZAUUHMCFcuYzGoZ1wG/kTRMICxsnW5AXohYMRxnyecP9ob42Gvn5TilhC0q66AtTPRSNMfw=="], "conventional-changelog-config-spec": ["conventional-changelog-config-spec@2.1.0", "", {}, "sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ=="], - "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@4.6.3", "", { "dependencies": { "compare-func": "^2.0.0", "lodash": "^4.17.15", "q": "^1.5.1" } }, "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g=="], + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@6.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw=="], - "conventional-changelog-core": ["conventional-changelog-core@4.2.4", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog-writer": "^5.0.0", "conventional-commits-parser": "^3.2.0", "dateformat": "^3.0.0", "get-pkg-repo": "^4.0.0", "git-raw-commits": "^2.0.8", "git-remote-origin-url": "^2.0.0", "git-semver-tags": "^4.1.1", "lodash": "^4.17.15", "normalize-package-data": "^3.0.0", "q": "^1.5.1", "read-pkg": "^3.0.0", "read-pkg-up": "^3.0.0", "through2": "^4.0.0" } }, "sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg=="], + "conventional-changelog-core": ["conventional-changelog-core@5.0.2", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog-writer": "^6.0.0", "conventional-commits-parser": "^4.0.0", "dateformat": "^3.0.3", "get-pkg-repo": "^4.2.1", "git-raw-commits": "^3.0.0", "git-remote-origin-url": "^2.0.0", "git-semver-tags": "^5.0.0", "normalize-package-data": "^3.0.3", "read-pkg": "^3.0.0", "read-pkg-up": "^3.0.0" } }, "sha512-RhQOcDweXNWvlRwUDCpaqXzbZemKPKncCWZG50Alth72WITVd6nhVk9MJ6w1k9PFNBcZ3YwkdkChE+8+ZwtUug=="], - "conventional-changelog-ember": ["conventional-changelog-ember@2.0.9", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A=="], + "conventional-changelog-ember": ["conventional-changelog-ember@3.0.0", "", {}, "sha512-7PYthCoSxIS98vWhVcSphMYM322OxptpKAuHYdVspryI0ooLDehRXWeRWgN+zWSBXKl/pwdgAg8IpLNSM1/61A=="], - "conventional-changelog-eslint": ["conventional-changelog-eslint@3.0.9", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA=="], + "conventional-changelog-eslint": ["conventional-changelog-eslint@4.0.0", "", {}, "sha512-nEZ9byP89hIU0dMx37JXQkE1IpMmqKtsaR24X7aM3L6Yy/uAtbb+ogqthuNYJkeO1HyvK7JsX84z8649hvp43Q=="], - "conventional-changelog-express": ["conventional-changelog-express@2.0.6", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ=="], + "conventional-changelog-express": ["conventional-changelog-express@3.0.0", "", {}, "sha512-HqxihpUMfIuxvlPvC6HltA4ZktQEUan/v3XQ77+/zbu8No/fqK3rxSZaYeHYant7zRxQNIIli7S+qLS9tX9zQA=="], - "conventional-changelog-jquery": ["conventional-changelog-jquery@3.0.11", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw=="], + "conventional-changelog-jquery": ["conventional-changelog-jquery@4.0.0", "", {}, "sha512-TTIN5CyzRMf8PUwyy4IOLmLV2DFmPtasKN+x7EQKzwSX8086XYwo+NeaeA3VUT8bvKaIy5z/JoWUvi7huUOgaw=="], - "conventional-changelog-jshint": ["conventional-changelog-jshint@2.0.9", "", { "dependencies": { "compare-func": "^2.0.0", "q": "^1.5.1" } }, "sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA=="], + "conventional-changelog-jshint": ["conventional-changelog-jshint@3.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-bQof4byF4q+n+dwFRkJ/jGf9dCNUv4/kCDcjeCizBvfF81TeimPZBB6fT4HYbXgxxfxWXNl/i+J6T0nI4by6DA=="], - "conventional-changelog-preset-loader": ["conventional-changelog-preset-loader@2.3.4", "", {}, "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g=="], + "conventional-changelog-preset-loader": ["conventional-changelog-preset-loader@3.0.0", "", {}, "sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA=="], - "conventional-changelog-writer": ["conventional-changelog-writer@5.0.1", "", { "dependencies": { "conventional-commits-filter": "^2.0.7", "dateformat": "^3.0.0", "handlebars": "^4.7.7", "json-stringify-safe": "^5.0.1", "lodash": "^4.17.15", "meow": "^8.0.0", "semver": "^6.0.0", "split": "^1.0.0", "through2": "^4.0.0" }, "bin": { "conventional-changelog-writer": "cli.js" } }, "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ=="], + "conventional-changelog-writer": ["conventional-changelog-writer@6.0.1", "", { "dependencies": { "conventional-commits-filter": "^3.0.0", "dateformat": "^3.0.3", "handlebars": "^4.7.7", "json-stringify-safe": "^5.0.1", "meow": "^8.1.2", "semver": "^7.0.0", "split": "^1.0.1" }, "bin": { "conventional-changelog-writer": "cli.js" } }, "sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ=="], - "conventional-commits-filter": ["conventional-commits-filter@2.0.7", "", { "dependencies": { "lodash.ismatch": "^4.4.0", "modify-values": "^1.0.0" } }, "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA=="], + "conventional-commits-filter": ["conventional-commits-filter@3.0.0", "", { "dependencies": { "lodash.ismatch": "^4.4.0", "modify-values": "^1.0.1" } }, "sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q=="], - "conventional-commits-parser": ["conventional-commits-parser@3.2.4", "", { "dependencies": { "JSONStream": "^1.0.4", "is-text-path": "^1.0.1", "lodash": "^4.17.15", "meow": "^8.0.0", "split2": "^3.0.0", "through2": "^4.0.0" }, "bin": { "conventional-commits-parser": "cli.js" } }, "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q=="], + "conventional-commits-parser": ["conventional-commits-parser@4.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^1.0.1", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "conventional-commits-parser": "cli.js" } }, "sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg=="], - "conventional-recommended-bump": ["conventional-recommended-bump@6.1.0", "", { "dependencies": { "concat-stream": "^2.0.0", "conventional-changelog-preset-loader": "^2.3.4", "conventional-commits-filter": "^2.0.7", "conventional-commits-parser": "^3.2.0", "git-raw-commits": "^2.0.8", "git-semver-tags": "^4.1.1", "meow": "^8.0.0", "q": "^1.5.1" }, "bin": { "conventional-recommended-bump": "cli.js" } }, "sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw=="], + "conventional-recommended-bump": ["conventional-recommended-bump@7.0.1", "", { "dependencies": { "concat-stream": "^2.0.0", "conventional-changelog-preset-loader": "^3.0.0", "conventional-commits-filter": "^3.0.0", "conventional-commits-parser": "^4.0.0", "git-raw-commits": "^3.0.0", "git-semver-tags": "^5.0.0", "meow": "^8.1.2" }, "bin": { "conventional-recommended-bump": "cli.js" } }, "sha512-Ft79FF4SlOFvX4PkwFDRnaNiIVX7YbmqGU0RwccUaiGvgp3S0a8ipR2/Qxk31vclDNM+GSdJOVs2KrsUCjblVA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -809,15 +917,19 @@ "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "daisyui": ["daisyui@5.5.14", "", {}, "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg=="], + "cycle": ["cycle@1.0.3", "", {}, "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA=="], + + "daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="], "dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="], @@ -833,6 +945,8 @@ "decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -843,6 +957,8 @@ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], @@ -851,6 +967,8 @@ "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -867,11 +985,9 @@ "dotgitignore": ["dotgitignore@2.1.0", "", { "dependencies": { "find-up": "^3.0.0", "minimatch": "^3.0.4" } }, "sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA=="], - "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], - "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], - - "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="], + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -883,7 +999,7 @@ "electron-to-chromium": ["electron-to-chromium@1.5.277", "", {}, "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw=="], - "elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="], + "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -893,7 +1009,7 @@ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -911,35 +1027,33 @@ "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - - "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - - "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fast-xml-builder": ["fast-xml-builder@1.1.9", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw=="], + + "fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -947,7 +1061,7 @@ "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], - "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], + "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -961,7 +1075,7 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -985,15 +1099,15 @@ "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], - "git-raw-commits": ["git-raw-commits@2.0.11", "", { "dependencies": { "dargs": "^7.0.0", "lodash": "^4.17.15", "meow": "^8.0.0", "split2": "^3.0.0", "through2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.js" } }, "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A=="], + "git-raw-commits": ["git-raw-commits@3.0.0", "", { "dependencies": { "dargs": "^7.0.0", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "git-raw-commits": "cli.js" } }, "sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw=="], "git-remote-origin-url": ["git-remote-origin-url@2.0.0", "", { "dependencies": { "gitconfiglocal": "^1.0.0", "pify": "^2.3.0" } }, "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw=="], - "git-semver-tags": ["git-semver-tags@4.1.1", "", { "dependencies": { "meow": "^8.0.0", "semver": "^6.0.0" }, "bin": { "git-semver-tags": "cli.js" } }, "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA=="], + "git-semver-tags": ["git-semver-tags@5.0.1", "", { "dependencies": { "meow": "^8.1.2", "semver": "^7.0.0" }, "bin": { "git-semver-tags": "cli.js" } }, "sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA=="], "gitconfiglocal": ["gitconfiglocal@1.0.0", "", { "dependencies": { "ini": "^1.3.2" } }, "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ=="], - "glob": ["glob@10.3.3", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" } }, "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw=="], + "glob": ["glob@6.0.4", "", { "dependencies": { "inflight": "^1.0.4", "inherits": "2", "minimatch": "2 || 3", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1007,7 +1121,7 @@ "gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="], - "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + "handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="], "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], @@ -1019,12 +1133,20 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + "howler": ["howler@2.2.4", "", {}, "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="], + "html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], @@ -1033,23 +1155,35 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], - "immutable": ["immutable@5.1.4", "", {}, "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA=="], + "immutable": ["immutable@5.1.5", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="], "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], @@ -1059,6 +1193,8 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], @@ -1067,7 +1203,7 @@ "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], - "is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "is-text-path": ["is-text-path@1.0.1", "", { "dependencies": { "text-extensions": "^1.0.0" } }, "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w=="], @@ -1079,9 +1215,11 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="], + "jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="], - "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], + "jimp": ["jimp@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/diff": "1.6.1", "@jimp/js-bmp": "1.6.1", "@jimp/js-gif": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/js-tiff": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/plugin-blur": "1.6.1", "@jimp/plugin-circle": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-contain": "1.6.1", "@jimp/plugin-cover": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-displace": "1.6.1", "@jimp/plugin-dither": "1.6.1", "@jimp/plugin-fisheye": "1.6.1", "@jimp/plugin-flip": "1.6.1", "@jimp/plugin-hash": "1.6.1", "@jimp/plugin-mask": "1.6.1", "@jimp/plugin-print": "1.6.1", "@jimp/plugin-quantize": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/plugin-rotate": "1.6.1", "@jimp/plugin-threshold": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1" } }, "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -1113,29 +1251,29 @@ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -1145,6 +1283,8 @@ "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], "lodash.defaultsdeep": ["lodash.defaultsdeep@4.6.1", "", {}, "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA=="], @@ -1159,6 +1299,8 @@ "lodash.negate": ["lodash.negate@3.0.2", "", {}, "sha512-JGJYYVslKYC0tRMm/7igfdHulCjoXjoganRNWM8AgS+RXfOvFnPkOveDhPI65F9aAypCX9QEEQoBqWf7Q6uAeA=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -1171,15 +1313,69 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], "meow": ["meow@8.1.2", "", { "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", "type-fest": "^0.18.0", "yargs-parser": "^20.2.3" } }, "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], @@ -1205,6 +1401,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -1221,18 +1419,18 @@ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - "node-downloader-helper": ["node-downloader-helper@2.1.10", "", { "bin": { "ndh": "bin/ndh" } }, "sha512-8LdieUd4Bqw/CzfZLf30h+1xSAq3riWSDfWKsPJYz8EULoWxjS1vw6BGLYFZDxQgXjDR7UmC9UpQ0oV93U98Fg=="], + "node-downloader-helper": ["node-downloader-helper@2.1.11", "", { "bin": { "ndh": "bin/ndh" } }, "sha512-882fH2C9AWdiPCwz/2beq5t8FGMZK9Dx8TJUOIxzMCbvG7XUKM5BuJwN5f0NKo4SCQK6jR4p2TPm54mYGdGchQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - "node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "node-stream-zip": ["node-stream-zip@1.15.0", "", {}, "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="], + "node-unrar-js": ["node-unrar-js@2.0.2", "", {}, "sha512-hLNmoJzqaKJnod8yiTVGe9hnlNRHotUi0CreSv/8HtfRi/3JnRC8DvsmKfeGGguRjTEulhZK6zXX5PXoVuDZ2w=="], + "normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -1251,16 +1449,24 @@ "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="], + "optimist": ["optimist@0.6.1", "", { "dependencies": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" } }, "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-queue": ["p-queue@9.2.0", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g=="], + + "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -1271,8 +1477,12 @@ "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -1281,6 +1491,10 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -1291,8 +1505,6 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], - "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -1305,12 +1517,16 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "pkginfo": ["pkginfo@0.3.1", "", {}, "sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A=="], + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], "portfinder": ["portfinder@1.0.38", "", { "dependencies": { "async": "^3.2.6", "debug": "^4.3.6" } }, "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], "preact": ["preact@10.11.3", "", {}, "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="], @@ -1323,37 +1539,37 @@ "pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="], - "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], "qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="], "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], - "react-error-boundary": ["react-error-boundary@6.1.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-02k9WQ/mUhdbXir0tC1NiMesGzRPaCsJEWU/4bcFrbY1YMZOtHShtZP6zw0SJrBWA/31H0KT9/FgdL8+sPKgHA=="], + "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="], "react-hot-toast": ["react-hot-toast@2.6.0", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "react-qr-code": ["react-qr-code@2.0.18", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + + "react-qr-code": ["react-qr-code@2.0.21", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-xaywjo0eaF4S3LOz6ns5eoPbM2E+q9HYl4VATYpxK4bBniOhQ9noY2RJ9G4SnZFhUwzx63FUT6KdHzfKgUwyuQ=="], "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], @@ -1363,79 +1579,75 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], - "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sass": ["sass@1.97.3", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg=="], + "sass": ["sass@1.99.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q=="], - "sass-embedded": ["sass-embedded@1.97.3", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.97.3", "sass-embedded-android-arm": "1.97.3", "sass-embedded-android-arm64": "1.97.3", "sass-embedded-android-riscv64": "1.97.3", "sass-embedded-android-x64": "1.97.3", "sass-embedded-darwin-arm64": "1.97.3", "sass-embedded-darwin-x64": "1.97.3", "sass-embedded-linux-arm": "1.97.3", "sass-embedded-linux-arm64": "1.97.3", "sass-embedded-linux-musl-arm": "1.97.3", "sass-embedded-linux-musl-arm64": "1.97.3", "sass-embedded-linux-musl-riscv64": "1.97.3", "sass-embedded-linux-musl-x64": "1.97.3", "sass-embedded-linux-riscv64": "1.97.3", "sass-embedded-linux-x64": "1.97.3", "sass-embedded-unknown-all": "1.97.3", "sass-embedded-win32-arm64": "1.97.3", "sass-embedded-win32-x64": "1.97.3" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA=="], + "sass-embedded": ["sass-embedded@1.99.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.1.5", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.99.0", "sass-embedded-android-arm": "1.99.0", "sass-embedded-android-arm64": "1.99.0", "sass-embedded-android-riscv64": "1.99.0", "sass-embedded-android-x64": "1.99.0", "sass-embedded-darwin-arm64": "1.99.0", "sass-embedded-darwin-x64": "1.99.0", "sass-embedded-linux-arm": "1.99.0", "sass-embedded-linux-arm64": "1.99.0", "sass-embedded-linux-musl-arm": "1.99.0", "sass-embedded-linux-musl-arm64": "1.99.0", "sass-embedded-linux-musl-riscv64": "1.99.0", "sass-embedded-linux-musl-x64": "1.99.0", "sass-embedded-linux-riscv64": "1.99.0", "sass-embedded-linux-x64": "1.99.0", "sass-embedded-unknown-all": "1.99.0", "sass-embedded-win32-arm64": "1.99.0", "sass-embedded-win32-x64": "1.99.0" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg=="], - "sass-embedded-all-unknown": ["sass-embedded-all-unknown@1.97.3", "", { "dependencies": { "sass": "1.97.3" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg=="], + "sass-embedded-all-unknown": ["sass-embedded-all-unknown@1.99.0", "", { "dependencies": { "sass": "1.99.0" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw=="], - "sass-embedded-android-arm": ["sass-embedded-android-arm@1.97.3", "", { "os": "android", "cpu": "arm" }, "sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg=="], + "sass-embedded-android-arm": ["sass-embedded-android-arm@1.99.0", "", { "os": "android", "cpu": "arm" }, "sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ=="], - "sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.97.3", "", { "os": "android", "cpu": "arm64" }, "sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA=="], + "sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.99.0", "", { "os": "android", "cpu": "arm64" }, "sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg=="], - "sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.97.3", "", { "os": "android", "cpu": "none" }, "sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA=="], + "sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.99.0", "", { "os": "android", "cpu": "none" }, "sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw=="], - "sass-embedded-android-x64": ["sass-embedded-android-x64@1.97.3", "", { "os": "android", "cpu": "x64" }, "sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw=="], + "sass-embedded-android-x64": ["sass-embedded-android-x64@1.99.0", "", { "os": "android", "cpu": "x64" }, "sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ=="], - "sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.97.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA=="], + "sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.99.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg=="], - "sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.97.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA=="], + "sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.99.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A=="], - "sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.97.3", "", { "os": "linux", "cpu": "arm" }, "sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA=="], + "sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.99.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw=="], - "sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.97.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg=="], + "sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.99.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew=="], - "sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.97.3", "", { "os": "linux", "cpu": "arm" }, "sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg=="], + "sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.99.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ=="], - "sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.97.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw=="], + "sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.99.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw=="], - "sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.97.3", "", { "os": "linux", "cpu": "none" }, "sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA=="], + "sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.99.0", "", { "os": "linux", "cpu": "none" }, "sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg=="], - "sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.97.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw=="], + "sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.99.0", "", { "os": "linux", "cpu": "x64" }, "sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew=="], - "sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.97.3", "", { "os": "linux", "cpu": "none" }, "sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA=="], + "sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.99.0", "", { "os": "linux", "cpu": "none" }, "sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA=="], - "sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.97.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg=="], + "sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.99.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA=="], - "sass-embedded-unknown-all": ["sass-embedded-unknown-all@1.97.3", "", { "dependencies": { "sass": "1.97.3" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q=="], + "sass-embedded-unknown-all": ["sass-embedded-unknown-all@1.99.0", "", { "dependencies": { "sass": "1.99.0" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg=="], - "sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.97.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw=="], + "sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.99.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw=="], - "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.97.3", "", { "os": "win32", "cpu": "x64" }, "sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw=="], + "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.99.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg=="], - "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -1443,9 +1655,9 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "seroval": ["seroval@1.4.2", "", {}, "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ=="], + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], - "seroval-plugins": ["seroval-plugins@1.4.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA=="], + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1465,18 +1677,24 @@ "simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="], + "slugify": ["slugify@1.6.9", "", {}, "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg=="], + + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + "socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="], "socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="], "socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], @@ -1489,7 +1707,7 @@ "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], - "standard-version": ["standard-version@9.5.0", "", { "dependencies": { "chalk": "^2.4.2", "conventional-changelog": "3.1.25", "conventional-changelog-config-spec": "2.1.0", "conventional-changelog-conventionalcommits": "4.6.3", "conventional-recommended-bump": "6.1.0", "detect-indent": "^6.0.0", "detect-newline": "^3.1.0", "dotgitignore": "^2.1.0", "figures": "^3.1.0", "find-up": "^5.0.0", "git-semver-tags": "^4.0.0", "semver": "^7.1.1", "stringify-package": "^1.0.1", "yargs": "^16.0.0" }, "bin": { "standard-version": "bin/cli.js" } }, "sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q=="], + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1497,7 +1715,7 @@ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "stringify-package": ["stringify-package@1.0.1", "", {}, "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1507,45 +1725,49 @@ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "svgo": ["svgo@3.3.2", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": "./bin/svgo" }, "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw=="], + "svg-icon-baker": ["svg-icon-baker@2.0.1", "", { "dependencies": { "css-tree": "^3.2.1", "fast-xml-builder": "^1.1.5", "fast-xml-parser": "^5.7.2", "svgo": "^4.0.1" } }, "sha512-1QWVlle2fSUra129CEKpo5sn4hLGa0KCd7v8kX+PkD8e1x8fAQXB5rkc8J8mXTHe6iSzcOS7j4Y2K+q9RaUoNQ=="], + + "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], "sync-child-process": ["sync-child-process@1.0.2", "", { "dependencies": { "sync-message-port": "^1.0.0" } }, "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA=="], "sync-message-port": ["sync-message-port@1.2.0", "", {}, "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg=="], - "systeminformation": ["systeminformation@5.31.1", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-6pRwxoGeV/roJYpsfcP6tN9mep6pPeCtXbUOCdVa0nme05Brwcwdge/fVNhIZn2wuUitAKZm4IYa7QjnRIa9zA=="], + "systeminformation": ["systeminformation@5.31.6", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], + "terser": ["terser@5.36.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="], "text-extensions": ["text-extensions@1.9.0", "", {}, "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ=="], "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], - - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - - "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], + "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], @@ -1561,7 +1783,7 @@ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], - "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], "tough-cookie-file-store": ["tough-cookie-file-store@3.3.0", "", { "dependencies": { "tough-cookie": "^6.0.0" } }, "sha512-FbO/cOi/jp4wweo8soVNG/ZjDsgpBZWqaxWwu7gRKvsjg/Qt44kStp87VLfJnin749DlTbZDYvV1wuSr5jly2g=="], @@ -1569,11 +1791,11 @@ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "trim-newlines": ["trim-newlines@3.0.1", "", {}, "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw=="], - "ts-apicalypse": ["ts-apicalypse@0.4.2", "", { "dependencies": { "axios": "^1.4.0" } }, "sha512-A02KFDFZHYTft0fTkxr5AERLb//XK/qjvGxrX8uNCwZAvFpLI/4TrOVxbq6kV2caZXWAn8eZZfRzVn1xd/cSMw=="], - - "ts-igdb-client": ["ts-igdb-client@0.4.2", "", { "dependencies": { "axios": "^1.4.0", "ts-apicalypse": "^0.4.2" } }, "sha512-VGQbIyy75GbHW0WGGHBXsdJzuj8wOW/jiWURmQWltpkFjCTMSqqx9gTPSUUcJ0W2kdlWcyMU4TDDHo4N3Fij7A=="], + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], @@ -1591,15 +1813,29 @@ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "underscore": ["underscore@1.8.3", "", {}, "sha512-5WsVTFcH1ut/kkhAaHf4PVgI8c7++GiVcpCGxPouI6ZVjsqPnSDf8h/8HtVqc0t4fzRXwnMK70EcZeAs3PIddg=="], + "undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "union": ["union@0.5.0", "", { "dependencies": { "qs": "^6.4.0" } }, "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + "unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], "unzip-stream": ["unzip-stream@0.3.4", "", { "dependencies": { "binary": "^0.3.0", "mkdirp": "^0.5.1" } }, "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw=="], @@ -1621,9 +1857,13 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - "vite-plugin-svg-icons-ng": ["vite-plugin-svg-icons-ng@1.5.2", "", { "dependencies": { "fast-glob": "^3.3.3", "fs-extra": "^11.3.2", "node-html-parser": "^7.0.1", "svgo": "^3.3.2" }, "peerDependencies": { "vite": ">=5.0.0" } }, "sha512-A68obs8XDT+q8q8dKyjrT/v0qw8h5pEBKXJ27aUXjARYeJ6MNvhIhRLLiUwnSrbn/B4TBF4UVaWRXKftAqP7+A=="], + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + + "vite-plugin-svg-icons-ng": ["vite-plugin-svg-icons-ng@1.9.1", "", { "dependencies": { "svg-icon-baker": "2.0.1", "tinyglobby": "^0.2.16" }, "peerDependencies": { "vite": ">=5.0.0" } }, "sha512-g00nlit2havo0VRxpLiPkeJfMYt0DL/RO8X5HHop72rbMEZB5H1Fk7qXLWbTIO2/PkwJ8zSq0+h28ItaE1YQHQ=="], "vite-static-assets-plugin": ["vite-static-assets-plugin@1.2.2", "", { "dependencies": { "chalk": "^5.4.1", "chokidar": "^3.5.3", "minimatch": "^10.0.1" }, "peerDependencies": { "typescript": "^5.0.0", "vite": "^6.2.0" } }, "sha512-0mzHsxFa46Np5AixQcdWYLVH6eJxeok7qL7tXmxYavg/Uo0e5z+J6gavJ0TJ6dmJSe2Z+gwmDb64bCCZfg+gqA=="], @@ -1645,12 +1885,16 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + "winston": ["winston@1.0.2", "", { "dependencies": { "async": "~1.0.0", "colors": "1.0.x", "cycle": "1.0.x", "eyes": "0.1.x", "isstream": "0.1.x", "pkginfo": "0.3.x", "stack-trace": "0.0.x" } }, "sha512-BLxJH3KCgJ2paj2xKYTQLpxdKr9URPDDDLJnRVcbud7izT+m8Xzt5Rod6mnNgEcfT0fRvhEy2Cj3cEnnQpa6qA=="], + + "wordwrap": ["wordwrap@0.0.3", "", {}, "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], @@ -1667,18 +1911,42 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@ap0nia/eden/elysia": ["elysia@1.2.15", "", { "dependencies": { "@sinclair/typebox": "^0.34.15", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-/oUSNb83jIWAGi6uSmbQ7Uy0RSJ9NimbVToSLnYS8jjsGId3zgdHqprsdf4rIMInOmEM8skjsFhZ4x8C5AB6+w=="], + + "@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@babel/template/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/template/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -1687,8 +1955,6 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "@jimp/core/file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], - "@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1725,41 +1991,53 @@ "@jimp/wasm-webp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@node-minify/core/glob": ["glob@10.3.3", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" } }, "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw=="], + "@node-minify/core/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], - "@node-minify/terser/terser": ["terser@5.36.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="], + "@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], + "@tanstack/router-generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@tanstack/router-core/@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], + "@tanstack/router-generator/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/router-utils/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "@types/babel__core/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@types/babel__template/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "babel-dead-code-elimination/@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + + "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], "compare-func/dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], - "conventional-changelog-writer/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], @@ -1773,15 +2051,13 @@ "engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "get-pkg-repo/through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], - "get-pkg-repo/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], - "git-semver-tags/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "gitconfiglocal/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "handlebars/wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -1789,8 +2065,14 @@ "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "http-proxy/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "http-server/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], + "is-core-module/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "load-json-file/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], "meow/read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], @@ -1799,10 +2081,14 @@ "meow/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], "nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], + "optimist/minimist": ["minimist@0.0.10", "", {}, "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -1811,36 +2097,46 @@ "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], + "portfinder/async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], "read-pkg-up/find-up": ["find-up@2.1.0", "", { "dependencies": { "locate-path": "^2.0.0" } }, "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ=="], - "readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "standard-version/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - - "standard-version/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], - "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "tough-cookie-file-store/tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], + "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "vite-plugin-svg-icons-ng/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "vite-static-assets-plugin/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "winston/async": ["async@1.0.0", "", {}, "sha512-5mO7DX4CbJzp9zjaFXusQQ4tzKJARjNB1Ih1pVBi8wkbmXy/xzIDgEMXxWePLzt2OdFwaxfneIlT1nCiXubrPQ=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + + "@ap0nia/eden/elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "@ap0nia/eden/elysia/memoirist": ["memoirist@0.3.1", "", {}, "sha512-lmk4Z45IuVZPT67nxAdD3rAsNExxMEBFXgCeJGJnoLkYOjmZnJ8Hmi+MGdl9oLKtAENFAAgG8FvV3Z8BNiqy8w=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1891,28 +2187,38 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "@jimp/core/file-type/strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], + "@node-minify/core/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "@jimp/core/file-type/token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "@tanstack/router-utils/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "@node-minify/terser/terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "babel-dead-code-elimination/@babel/core/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "babel-dead-code-elimination/@babel/core/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "babel-dead-code-elimination/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "concurrently/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "dotgitignore/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "get-pkg-repo/through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "get-pkg-repo/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "get-pkg-repo/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "http-server/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "http-server/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "meow/read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "meow/read-pkg-up/read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", "parse-json": "^5.0.0", "type-fest": "^0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], @@ -1927,13 +2233,7 @@ "sass/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "standard-version/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "standard-version/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "standard-version/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], - - "standard-version/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], @@ -1987,6 +2287,8 @@ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "vite-plugin-svg-icons-ng/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], @@ -2039,11 +2341,19 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@node-minify/core/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "concurrently/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "dotgitignore/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], "dotgitignore/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "get-pkg-repo/through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "http-server/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "meow/read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -2057,12 +2367,16 @@ "read-pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "standard-version/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "standard-version/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concurrently/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "dotgitignore/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "http-server/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "meow/read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "meow/read-pkg-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], @@ -2071,8 +2385,6 @@ "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@1.3.0", "", { "dependencies": { "p-try": "^1.0.0" } }, "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q=="], - "standard-version/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "meow/read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "read-pkg-up/find-up/locate-path/p-locate/p-limit/p-try": ["p-try@1.0.0", "", {}, "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww=="], diff --git a/drizzle/0002_flowery_rocket_raccoon.sql b/drizzle/0002_flowery_rocket_raccoon.sql new file mode 100644 index 0000000..5f7942e --- /dev/null +++ b/drizzle/0002_flowery_rocket_raccoon.sql @@ -0,0 +1,31 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_games` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `source_id` text, + `source` text, + `igdb_id` integer, + `name` text, + `ra_id` integer, + `path_fs` text, + `main_glob` text, + `last_played` integer, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `metadata` text DEFAULT '{}' NOT NULL, + `slug` text, + `platform_id` integer NOT NULL, + `cover` blob, + `type` text, + `summary` text, + `version` text, + `version_source` text, + `version_system` text, + FOREIGN KEY (`platform_id`) REFERENCES `platforms`(`id`) ON UPDATE cascade ON DELETE no action +); +--> statement-breakpoint +INSERT INTO `__new_games`("id", "source_id", "source", "igdb_id", "name", "ra_id", "path_fs", "main_glob", "last_played", "created_at", "metadata", "slug", "platform_id", "cover", "type", "summary", "version", "version_source", "version_system") SELECT "id", "source_id", "source", "igdb_id", "name", "ra_id", "path_fs", NULL, "last_played", "created_at", "metadata", "slug", "platform_id", "cover", "type", "summary", NULL, NULL, NULL FROM `games`;--> statement-breakpoint +DROP TABLE `games`;--> statement-breakpoint +ALTER TABLE `__new_games` RENAME TO `games`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `games_igdb_id_unique` ON `games` (`igdb_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `games_ra_id_unique` ON `games` (`ra_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `games_slug_unique` ON `games` (`slug`); \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..fb182f2 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,479 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "40569ae5-facd-4680-bd48-fe70c5abf498", + "prevId": "6dc00b41-64d7-4cb3-ad90-f9e8e35af643", + "tables": { + "collections": { + "name": "collections", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "collections_games": { + "name": "collections_games", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_id": { + "name": "game_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "collections_games_collection_id_collections_id_fk": { + "name": "collections_games_collection_id_collections_id_fk", + "tableFrom": "collections_games", + "tableTo": "collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "collections_games_game_id_games_id_fk": { + "name": "collections_games_game_id_games_id_fk", + "tableFrom": "collections_games", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "games": { + "name": "games", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "igdb_id": { + "name": "igdb_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ra_id": { + "name": "ra_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path_fs": { + "name": "path_fs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_glob": { + "name": "main_glob", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_played": { + "name": "last_played", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "platform_id": { + "name": "platform_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cover": { + "name": "cover", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version_source": { + "name": "version_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version_system": { + "name": "version_system", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "games_igdb_id_unique": { + "name": "games_igdb_id_unique", + "columns": [ + "igdb_id" + ], + "isUnique": true + }, + "games_ra_id_unique": { + "name": "games_ra_id_unique", + "columns": [ + "ra_id" + ], + "isUnique": true + }, + "games_slug_unique": { + "name": "games_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "games_platform_id_platforms_id_fk": { + "name": "games_platform_id_platforms_id_fk", + "tableFrom": "games", + "tableTo": "platforms", + "columnsFrom": [ + "platform_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "platforms": { + "name": "platforms", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "igdb_id": { + "name": "igdb_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "igdb_slug": { + "name": "igdb_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "moby_id": { + "name": "moby_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "es_slug": { + "name": "es_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ra_id": { + "name": "ra_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover": { + "name": "cover", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "platforms_igdb_id_unique": { + "name": "platforms_igdb_id_unique", + "columns": [ + "igdb_id" + ], + "isUnique": true + }, + "platforms_igdb_slug_unique": { + "name": "platforms_igdb_slug_unique", + "columns": [ + "igdb_slug" + ], + "isUnique": true + }, + "platforms_moby_id_unique": { + "name": "platforms_moby_id_unique", + "columns": [ + "moby_id" + ], + "isUnique": true + }, + "platforms_es_slug_unique": { + "name": "platforms_es_slug_unique", + "columns": [ + "es_slug" + ], + "isUnique": true + }, + "platforms_ra_id_unique": { + "name": "platforms_ra_id_unique", + "columns": [ + "ra_id" + ], + "isUnique": true + }, + "platforms_slug_unique": { + "name": "platforms_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "screenshots": { + "name": "screenshots", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "game_id": { + "name": "game_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "screenshots_game_id_games_id_fk": { + "name": "screenshots_game_id_games_id_fk", + "tableFrom": "screenshots", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c181729..0df44e3 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1772998956867, "tag": "0001_outstanding_silk_fever", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1776111721964, + "tag": "0002_flowery_rocket_raccoon", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index d07f8d3..913be66 100644 --- a/package.json +++ b/package.json @@ -1,118 +1,162 @@ { "name": "com.simeonradivoev.gameflow-deck", "displayName": "Gameflow", - "version": "1.2.0", + "author": { + "name": "Simeon Radivoev", + "email": "work@simeonradivoev.com", + "url": "https://simeonradivoev.com" + }, + "version": "1.6.0", "description": "Game Launcher", "icon": "./src/mainview/assets/icon.svg", "main": "./src/bun/index.ts", "bin": "gameflow", + "license": "AGPL-3.0", "repository": { "type": "git", "url": "https://github.com/simeonradivoev/gameflow-deck" }, "packageManager": "bun@1.3.9", "type": "module", + "workspaces": [ + "./src/packages/gameflow-sdk" + ], "scripts": { - "dev": "NODE_ENV=development bun run build:vite && bun run ./scripts/dev.ts", + "dev": "NODE_ENV=development bun run build:vite && conc 'bun run ./scripts/dev.ts'", "dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run ./scripts/dev.ts'", - "build:vite": "vite build", + "dev:bun:hmr": "PUBLIC_ACCESS=true NODE_ENV=development conc 'bun run hmr' 'bun run --watch ./src/bun/index.ts'", + "dev:bun": "NODE_ENV=development bun run build:vite && conc 'bun run ./src/bun/index.ts'", + "build:vite": "bun run --bun vite build", "build:prod:vite": "NODE_ENV=production bun run build:vite", "build:dev:vite": "NODE_ENV=development bun run build:vite", "build": "bun run build:vite && bun run ./scripts/package-bun.ts", + "build:non-compiled": "bun run build:vite && NON_COMPILED=true bun run ./scripts/package-bun.ts", "build:prod": "NODE_ENV=production bun run build", - "build:prod:dynamic": "NODE_ENV=production NON_COMPILED=true bun run build", "build:linux": "TARGET=bun-linux-x64 bun run build", "openapi-ts": "bun run ./scripts/romm/openapi-ts.ts", "run:build-action": "act workflow_dispatch --artifact-server-path artifacts --env ACTIONS_RUNTIME_TOKEN=foo -W .forgejo/workflows/build.yml", - "hmr": "vite --port 5173", + "hmr": "bun run --bun vite --port 5173", "drizzle:generate": "bunx drizzle-kit generate", "test": "bun test", "mappings:generate": "bun run drizzle-kit generate --dialect=sqlite --schema=./src/bun/api/schema/emulators.ts --out=./scripts/drizzle/es-de && bun run ./scripts/generate-es-de-mapping.ts", "flatpak:generate-sources": "bun run ./scripts/generate-flatpak-sources.ts", "flatpak:override": "flatpak override org.flatpak.Builder --filesystem=host --device=all", "flatpak:restore": "flatpak override --reset --user org.flatpak.Builder", - "flatpak:build": "flatpak run org.flatpak.Builder build/flatpak flatpak/com.simeonradivoev.gameflow-deck.json --repo=.config/flatpak/repo --force-clean", + "flatpak:build": "FLATPAK_BUILD=true NODE_ENV=production NON_COMPILED=true bun run build && flatpak run org.flatpak.Builder ../gameflow-flatpak/build/flatpak .config/flatpak/com.simeonradivoev.gameflow-deck.json --repo=.config/flatpak/repo --state-dir=../gameflow-flatpak/state --force-clean", "flatpak:install": "bun run flatpak:build && flatpak --user install --reinstall \"$PWD/.config/flatpak/repo\" com.simeonradivoev.gameflow-deck", "build:prod:appimage": "bun run build:prod && bun run ./scripts/build-appimage.ts", "build:dev:appimage": "bun run build && bun run ./scripts/build-appimage.ts", - "version:generate": "standard-version --sign", + "version:generate": "commit-and-tag-version --sign", "package:Linux": "bun run build:prod:appimage", - "package:Windows": "bun run build:prod" + "package:Windows": "bun run build:prod", + "download:chromium": "bun scripts/download-chromium.ts --out=./bin/chromium", + "download:nwjs": "bun scripts/download-nw.ts", + "build:audiosprites": "bun ./scripts/generate-audio-sprites.ts", + "tsc": "tsc --noEmit", + "publish:sdk": "bun publish --cwd ./src/packages/gameflow-sdk/ --access public" }, "dependencies": { + "7zip-bin": "^5.2.0", "@auth/core": "^0.34.3", - "@elysiajs/cors": "^1.4.1", - "@elysiajs/eden": "^1.4.6", - "@elysiajs/static": "^1.4.7", - "@jimp/wasm-webp": "^1.6.0", + "@elysiajs/cors": "^1.4.2", + "@elysiajs/eden": "^1.4.9", + "@jimp/wasm-webp": "^1.6.1", + "@phalcode/ts-igdb-client": "^1.0.26", "cheerio": "^1.2.0", - "conf": "^15.0.2", - "drizzle-orm": "^0.45.1", - "elysia": "^1.4.22", - "fs-extra": "^11.3.3", + "conf": "^15.1.0", + "drizzle-orm": "^0.45.2", + "elysia": "^1.4.28", + "fs-extra": "^11.3.5", "get-folder-size": "^5.0.0", - "jimp": "^1.6.0", + "ini": "^6.0.0", + "jimp": "^1.6.1", + "mustache": "^4.2.0", + "node-7z": "^3.0.0", "node-disk-info": "^1.3.0", - "node-downloader-helper": "^2.1.10", + "node-downloader-helper": "^2.1.11", "node-stream-zip": "^1.15.0", + "node-unrar-js": "^2.0.2", "open": "^11.0.0", + "p-queue": "^9.2.0", "pathe": "^2.0.3", - "systeminformation": "^5.31.1", - "tough-cookie": "^6.0.0", + "slugify": "^1.6.9", + "smol-toml": "^1.6.1", + "systeminformation": "^5.31.6", + "tapable": "^2.3.3", + "tough-cookie": "^6.0.1", "tough-cookie-file-store": "^3.3.0", - "ts-igdb-client": "^0.4.2", "unzip-stream": "^0.3.4", "webview-bun": "^2.4.0", - "zod": "^4.3.6" + "zod": "^4.4.3" + }, + "overrides": { + "@tanstack/router-generator": { + "zod": "^3.23.8" + } }, "devDependencies": { - "@ap0nia/eden": "^1.0.0-next.22", + "@ap0nia/eden": "^1.6.1", "@ap0nia/eden-tanstack-query": "^1.0.0-next.22", "@emulatorjs/emulatorjs": "^4.2.3", - "@hey-api/openapi-ts": "^0.91.0", - "@noriginmedia/norigin-spatial-navigation": "^2.3.0", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-form": "^1.28.0", - "@tanstack/react-query": "^5.90.20", - "@tanstack/react-query-devtools": "^5.91.3", - "@tanstack/react-router": "^1.157.16", - "@tanstack/react-router-devtools": "^1.154.12", - "@tanstack/react-router-ssr-query": "^1.157.17", - "@tanstack/router-plugin": "^1.157.16", - "@tanstack/zod-adapter": "^1.162.4", + "@hey-api/openapi-ts": "^0.91.1", + "@noriginmedia/norigin-spatial-navigation": "^3.1.0", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-form": "^1.32.0", + "@tanstack/react-query": "^5.100.10", + "@tanstack/react-query-devtools": "^5.100.10", + "@tanstack/react-query-persist-client": "^5.100.10", + "@tanstack/react-router": "^1.169.2", + "@tanstack/react-router-devtools": "^1.166.13", + "@tanstack/react-router-ssr-query": "^1.166.12", + "@tanstack/router-plugin": "^1.167.35", + "@tanstack/zod-adapter": "^1.166.9", + "@types/adm-zip": "^0.5.8", + "@types/audiosprite": "^0.7.3", "@types/bun": "latest", "@types/fs-extra": "^11.0.4", - "@types/react": "^19.2.9", + "@types/howler": "^2.2.12", + "@types/ini": "^4.1.1", + "@types/json-schema": "^7.0.15", + "@types/mustache": "^4.2.6", + "@types/node-7z": "^2.1.11", + "@types/rclone.js": "^0.6.3", + "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/unzip-stream": "^0.3.4", - "@vitejs/plugin-react": "^5.1.2", + "@vitejs/plugin-react": "^5.2.0", + "adm-zip": "^0.5.17", "animate.css": "^4.1.1", "app-builder-bin": "^5.0.0-alpha.13", + "audiosprite": "^0.7.2", "babel-plugin-react-compiler": "^1.0.0", "classnames": "^2.5.1", + "commit-and-tag-version": "^12.7.3", "concurrently": "^9.2.1", "cross-env": "^10.1.0", - "daisyui": "^5.5.14", - "drizzle-kit": "^0.31.9", - "dts-bundle-generator": "^9.5.1", + "daisyui": "^5.5.19", + "drizzle-kit": "^0.31.10", "eden-tanstack-query": "^0.0.9", + "howler": "^2.2.4", + "idb-keyval": "^6.2.2", "lucide-react": "^0.563.0", "pretty-bytes": "^7.1.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-error-boundary": "^6.1.0", + "pretty-ms": "^9.3.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-error-boundary": "^6.1.1", "react-hot-toast": "^2.6.0", - "react-qr-code": "^2.0.18", - "sass-embedded": "^1.97.3", - "standard-version": "^9.5.0", - "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.18", + "react-markdown": "^10.1.0", + "react-qr-code": "^2.0.21", + "sass-embedded": "^1.99.0", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "usehooks-ts": "^3.1.1", - "vite": "^7.3.1", - "vite-plugin-svg-icons-ng": "^1.5.2", + "vite": "^7.3.3", + "vite-plugin-svg-icons-ng": "^1.9.1", "vite-static-assets-plugin": "^1.2.2", "vite-tsconfig-paths": "^6.1.1" } -} +} \ No newline at end of file diff --git a/scripts/build-appimage.ts b/scripts/build-appimage.ts index 447c32d..c2f07f5 100644 --- a/scripts/build-appimage.ts +++ b/scripts/build-appimage.ts @@ -4,17 +4,12 @@ import fs from 'node:fs/promises'; import { appBuilderPath, } from 'app-builder-bin'; import path from 'node:path'; import { ensureDir } from "fs-extra"; -import { rmdir } from "node:fs"; +import mustache from "mustache"; -// ───────────────────────────────────────────── -// CONFIGURE THESE FOR YOUR APP -// ───────────────────────────────────────────── const APP_DIR = process.env.BUILD_DIR ?? `./build/${process.platform}`; const BINARY_NAME = pkg.bin; -const ICON = "./src/mainview/assets/256x256.png"; -const DESKTOP = "./flatpak/com.simeonradivoev.gameflow-deck.desktop"; +const ICON = "./src/mainview/public/256x256.png"; const TMP_FOLDER = "."; -// ───────────────────────────────────────────── const APP_NAME = pkg.displayName ?? pkg.name; const APP_ID = pkg.name; @@ -23,31 +18,54 @@ const APPDIR = path.resolve(TMP_FOLDER, `${APP_ID}.AppDir`); console.log(`>>> Building AppImage for ${APP_NAME} (${APP_ID})...`); await ensureDir(path.join(APPDIR, `usr`, 'bin')); +await ensureDir(path.join(APPDIR, `usr`, 'lib')); await ensureDir("build"); // Copy app dir await fs.cp(`${APP_DIR}/.`, path.join(APPDIR, `usr`, 'share'), { recursive: true }); await fs.rename(path.join(APPDIR, `usr`, 'share', BINARY_NAME), path.join(APPDIR, `usr`, 'bin', BINARY_NAME)); await fs.rename(path.join(APPDIR, `usr`, 'share', `libwebview-${process.arch}.so`), path.join(APPDIR, `usr`, 'lib', `libwebview-${process.arch}.so`)); +await fs.rename(path.join(APPDIR, `usr`, 'share', `7za`), path.join(APPDIR, `usr`, 'bin', `7za`)); -await fs.writeFile(path.join(APPDIR, `${APP_ID}.desktop`), `[Desktop Entry] -Version=${pkg.version} -X-AppImage-Name=${APP_NAME} -X-AppImage-Version=${pkg.version} -X-AppImage-Arch=${process.arch} -Name=${APP_NAME} -Comment=${pkg.description} -Exec=${APP_ID}.AppImage -Icon=.DirIcon -Type=Application -Categories=Game; -`); +if (!await fs.exists('./bin/nw/nw')) +{ + await import('./download-nw'); +} -await Bun.write(path.join(APPDIR, "AppRun"), `#!/bin/bash -APPDIR="$(dirname "$(readlink -f "$0")")" -APPIMAGE=true -exec "$APPDIR/usr/bin/${BINARY_NAME}" "$@" -`); +await ensureDir(path.join(APPDIR, `usr`, 'lib', 'nw')); +await fs.cp('./bin/nw', path.join(APPDIR, `usr`, 'lib', 'nw'), { recursive: true }); +await fs.symlink(path.join(APPDIR, `usr`, 'lib', 'nw', 'nw'), path.join(APPDIR, `usr`, `bin`, 'nw')); + +const templateVars = { + APP_NAME, + VERSION: pkg.version, + ARCH: process.arch, + DESCRIPTION: pkg.description, + APP_ID, + BINARY_NAME, + LICENSE: pkg.license +}; + +const desktopFileTemplate = await fs.readFile('./.config/appimage/com.simeonradivoev.gameflow-deck.desktop', 'utf8'); + +const raw = await $`git tag --sort=-version:refname`.text().then(d => d.trim()); +const tags = raw.split('\n').filter(t => t.match(/^\d+\.\d+\.\d+$/)); +console.log("tags", tags); + +console.log(">>> Updating Release History..."); +const releases = await Promise.all(tags.map(async tag => +{ + const date = await $`git log -1 --format=%as ${tag}`.text().then(d => d.trim()); + const version = tag.replace(/^v/, ''); + return ` `; +})); + +const appStreamTemplate = await fs.readFile('./.config/appimage/com.simeonradivoev.gameflow-deck.appdata.xml', 'utf8'); +await ensureDir(path.join(APPDIR, 'usr', 'share', 'metainfo')); +await fs.writeFile(path.join(APPDIR, 'usr', 'share', 'metainfo', `${APP_ID}.appdata.xml`), mustache.render(appStreamTemplate, { ...templateVars, RELEASES: releases })); + +const appRunTemplate = await fs.readFile(`./.config/appimage/AppRun`, 'utf8'); +await Bun.write(path.join(APPDIR, "AppRun"), mustache.render(appRunTemplate, templateVars)); await $`chmod +x ${APPDIR}/AppRun`; console.log(">>> Building AppImage..."); @@ -55,7 +73,7 @@ const config = { productName: pkg.displayName, productFilename: pkg.name, executableName: BINARY_NAME, - desktopEntry: DESKTOP, + desktopEntry: mustache.render(desktopFileTemplate, templateVars), icons: [ { file: ICON, @@ -70,7 +88,7 @@ const config = { // Remove the build dir, mainly to help with CIs await fs.rm(APP_DIR, { recursive: true }); await ensureDir(APP_DIR); -const OUTPUT = path.resolve(APP_DIR, `${APP_NAME}.AppImage`); +const OUTPUT = path.resolve(APP_DIR, `${APP_NAME}-${process.platform}-${process.arch}.AppImage`); const STAGE = path.resolve(TMP_FOLDER, `${APP_ID}.stage`); await ensureDir(STAGE); @@ -89,8 +107,9 @@ const proc = Bun.spawn([ }); const code = await proc.exited; -await fs.rm(APPDIR, { recursive: true, force: true }); await fs.rm(STAGE, { recursive: true, force: true }); +await fs.rm(APPDIR, { recursive: true, force: true }); + if (code !== 0) process.exit(code); -console.log(`\n✅ Done!`); \ No newline at end of file +console.log(`\n Done!`); \ No newline at end of file diff --git a/scripts/dev.ts b/scripts/dev.ts index 96ac9f2..1331f36 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -1,48 +1,48 @@ -// watcher.ts - run this instead of --watch import EventEmitter from "events"; import browser from '../src/bun/browser'; import { tmpdir } from "os"; import path from "path"; +import { watch } from "fs"; +import { sleep } from "bun"; const events = new EventEmitter(); const abortController = new AbortController(); +let restarting = false; process.env.WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = "--remote-debugging-port=9222"; process.env.NODE_ENV = "development"; -let retries = 0; - function spawnServer () { - return Bun.spawn(["bun", "run", '--watch', "--inspect=127.0.0.1:9228/fixed-session", "./src/bun/index.ts"], { + const s = Bun.spawn(["bun", '--install=fallback', "run", "--inspect=127.0.0.1:9228/fixed-session", "./src/bun/index.ts"], { env: { - ...Bun.env, + ...process.env, HEADLESS: "true", }, - stdout: "inherit", - stderr: "inherit", - stdin: "pipe", + stdout: 'inherit', + stderr: 'inherit', + stdin: 'inherit', signal: abortController.signal, - killSignal: 'SIGUSR1', + killSignal: 'SIGKILL', ipc (message, subprocess, handle) { - if (message.type === 'exitapp') + if (message === 'focus') + { + events.emit('focus'); + } else if (message === 'exitapp') { events.emit('exitapp'); } }, onExit (subprocess, exitCode, signalCode) { - if (exitCode === 1 && retries <= 3) - { - server = spawnServer(); - retries++; - } else + if (!restarting) { + console.log("Existing Dev With", exitCode); process.exit(); } - } }); + return s; } function spawnBrowser () @@ -50,17 +50,55 @@ function spawnBrowser () try { - return browser(events, Bun.env.FORCE_BROWSER === "true", { configPath: path.join(tmpdir(), 'gameflow') }); + return browser(events, { + configPath: path.join(tmpdir(), 'gameflow'), + isSteamDeckGameMode: false, + forceBrowser: process.env.FORCE_BROWSER === "true" + }); } catch (error) { console.error(error); }; } -let server = spawnServer(); -spawnBrowser()?.then(async e => +async function restart () { - console.log("Sending exit Signal to server"); - await server.stdin.write('shutdown\n'); - await server.stdin.flush(); -}); \ No newline at end of file + if (server) + { + restarting = true; + server.kill(); + await server.exited; + server = undefined; + console.log("Old Server stopped"); + } + + server = spawnServer(); + await sleep(1000); + console.log("New Server started"); + restarting = false; +} + +watch("./src/bun", { recursive: true }, (event, filename) => +{ + if (restarting) return; + console.log(`[watcher] ${event}: ${filename} — restarting...`); + restart(); +}); + +watch("./src/packages", { recursive: true }, (event, filename) => +{ + if (restarting) return; + console.log(`[watcher] ${event}: ${filename} — restarting...`); + restart(); +}); + +let server: Bun.Subprocess | undefined = spawnServer(); +if (!process.env.HEADLESS) +{ + spawnBrowser()?.then(async e => + { + if (!server) return; + abortController.abort(); + await server.exited; + }); +} \ No newline at end of file diff --git a/scripts/download-chromium.ts b/scripts/download-chromium.ts new file mode 100644 index 0000000..1e4a1c3 --- /dev/null +++ b/scripts/download-chromium.ts @@ -0,0 +1,283 @@ +#!/usr/bin/env bun +/** + * download-chromium.ts + * Downloads the latest ungoogled-chromium for the current platform + arch. + * Skips the download if the binary is already present and up to date. + * + * Usage: bun download-chromium.ts [--out=./chromium] [--force] + * In package.json scripts: "prebuild": "bun scripts/download-chromium.ts" + */ + +import { mkdir } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import StreamZip from "node-stream-zip"; + +// --- Config ------------------------------------------------------------------ + +const GITHUB_API = "https://api.github.com"; +const VERSION_FILE = ".chromium-version"; + +const REPOS: Record = { + linux: "ungoogled-software/ungoogled-chromium-portablelinux", + darwin: "ungoogled-software/ungoogled-chromium-macos", + win32: "ungoogled-software/ungoogled-chromium-windows", +}; + +const PLATFORM_MAP: Record = { + linux: "linux", + win32: "windows", + darwin: 'macos' +}; + +const ARCH_MAP: Record> = { + linux: { x64: "x86_64", arm64: "arm64" }, + darwin: { x64: "x86_64", arm64: "arm64" }, + win32: { x64: "x64", arm64: "arm64" }, +}; + +const PREFERRED_EXT: Record = { + linux: [".tar.xz"], + darwin: [".dmg", ".zip"], + win32: [".zip"], +}; + +/** The expected binary path per platform after extraction */ +function getBinaryPath (outDir: string, version: string, platform: string, arch: string): string +{ + const subFolder = `ungoogled-chromium_${version}_${PLATFORM_MAP[platform]}_${ARCH_MAP[platform][arch]}`; + if (platform === "linux") + { + return path.join(outDir, subFolder, "chrome"); + } + if (platform === "darwin") return path.join(outDir, "Chromium.app"); + return path.join(outDir, subFolder, "chrome.exe"); +} + +// --- Helpers ----------------------------------------------------------------- + +function log (msg: string) +{ + process.stdout.write(`\x1b[36m[chromium]\x1b[0m ${msg}\n`); +} + +function err (msg: string): never +{ + process.stderr.write(`\x1b[31m[error]\x1b[0m ${msg}\n`); + process.exit(1); +} + +async function ghFetch (url: string) +{ + const headers: Record = { "User-Agent": "bun-chromium-downloader" }; + const token = process.env.GITHUB_TOKEN; + if (token) headers["Authorization"] = `Bearer ${token}`; + const res = await fetch(url, { headers }); + if (!res.ok) err(`GitHub API error ${res.status}: ${url}`); + return res.json(); +} + +async function readVersionCache (outDir: string): Promise +{ + const file = path.join(outDir, VERSION_FILE); + if (!existsSync(file)) return null; + return (await Bun.file(file).text()).trim(); +} + +async function writeVersionCache (outDir: string, version: string) +{ + await Bun.write(path.join(outDir, VERSION_FILE), version); +} + +async function downloadWithProgress (url: string, dest: string) +{ + log(`Downloading -> ${dest}`); + const res = await fetch(url); + if (!res.ok) err(`Download failed: ${res.status} ${url}`); + + const total = Number(res.headers.get("content-length") ?? 0); + let received = 0; + const writer = Bun.file(dest).writer(); + const reader = res.body!.getReader(); + + while (true) + { + const { done, value } = await reader.read(); + if (done) break; + writer.write(value); + received += value.length; + if (total > 0) + { + const pct = ((received / total) * 100).toFixed(1); + const mb = (received / 1e6).toFixed(1); + const totalMb = (total / 1e6).toFixed(1); + process.stdout.write(`\r ${pct}% ${mb} / ${totalMb} MB `); + } + } + await writer.end(); + process.stdout.write("\n"); + log("Download complete."); +} + +async function extractZip (src: string, outDir: string) +{ + log(`Extracting zip -> ${outDir}`); + const zip = new StreamZip.async({ file: src }); + const entries = await zip.entries(); + const total = Object.keys(entries).length; + await zip.extract(null, outDir); + await zip.close(); + log(`Extracted ${total} files.`); +} + +function extractNative (src: string, outDir: string) +{ + if (src.endsWith(".AppImage")) + { + const dest = path.join(outDir, "chromium.AppImage"); + spawnSync("cp", [src, dest]); + spawnSync("chmod", ["+x", dest]); + log(`AppImage ready at: ${dest}`); + return; + } + + if (src.endsWith(".tar.xz")) + { + const result = spawnSync("tar", ["-xJf", src, "-C", outDir], { stdio: "inherit" }); + if (result.status !== 0) err("tar extraction failed"); + return; + } + + if (src.endsWith(".dmg")) + { + log("Mounting DMG..."); + const mount = spawnSync("hdiutil", ["attach", src, "-nobrowse", "-quiet"], { + encoding: "utf8", + }); + if (mount.status !== 0) err("hdiutil mount failed"); + const mountLine = mount.stdout.split("\n").find((l) => l.includes("/Volumes/")); + const mountPoint = mountLine?.split("\t").at(-1)?.trim(); + if (!mountPoint) err("Could not find DMG mount point"); + spawnSync("cp", ["-R", mountPoint!, outDir], { stdio: "inherit" }); + spawnSync("hdiutil", ["detach", mountPoint!, "-quiet"]); + log(`DMG contents copied to: ${outDir}`); + return; + } + + err(`Unknown archive format: ${src}`); +} + +// --- Main -------------------------------------------------------------------- + +async function main () +{ + const platform = process.platform; + const arch = process.arch; + const force = process.argv.includes("--force"); + const outArg = process.argv.find(a => a.startsWith("--out="))?.slice(6) + ?? "./chromium"; + const outDir = path.resolve(outArg); + + log(`Platform: ${platform} Arch: ${arch}`); + + const repo = REPOS[platform]; + if (!repo) err(`Unsupported platform: ${platform}`); + + const archStr = ARCH_MAP[platform]?.[arch]; + if (!archStr) err(`Unsupported arch "${arch}" on ${platform}`); + + // Fetch latest version (lightweight — just the tag, no asset download yet) + log(`Checking latest release from ${repo}...`); + const release = await ghFetch(`${GITHUB_API}/repos/${repo}/releases/latest`); + const version: string = release.tag_name ?? release.name ?? "unknown"; + log(`Latest version: ${version}`); + + // Check if already downloaded and up to date + if (!force) + { + const cachedVersion = await readVersionCache(outDir); + const assets: Array<{ name: string; }> = release.assets ?? []; + const preferred = PREFERRED_EXT[platform] ?? []; + let assetName: string | undefined; + for (const ext of preferred) + { + assetName = assets.find(a => a.name.includes(archStr) && a.name.endsWith(ext))?.name; + if (assetName) break; + } + if (!assetName) assetName = assets.find(a => a.name.includes(archStr))?.name; + + if (cachedVersion === version) + { + const binaryPath = getBinaryPath(outDir, cachedVersion, platform, arch); + if (existsSync(binaryPath)) + { + log(`Already up to date (${version}). Skipping download.`); + log(`Binary: ${binaryPath}`); + return; + } else + { + log(`Version matches but binary missing — re-downloading.`); + } + } else if (cachedVersion) + { + log(`New version available: ${cachedVersion} -> ${version}`); + } + } else + { + log("--force flag set, re-downloading."); + } + + // Pick asset to download + const assets: Array<{ name: string; browser_download_url: string; }> = release.assets ?? []; + if (assets.length === 0) err("No assets found in the latest release."); + + const preferred = PREFERRED_EXT[platform] ?? []; + let chosen: (typeof assets)[0] | undefined; + + for (const ext of preferred) + { + chosen = assets.find(a => a.name.includes(archStr) && a.name.endsWith(ext)); + if (chosen) break; + } + if (!chosen) chosen = assets.find(a => a.name.includes(archStr)); + + if (!chosen) + { + log("Available assets:"); + for (const a of assets) log(` ${a.name}`); + err(`No asset found matching arch "${archStr}" on ${platform}.`); + } + + log(`Selected asset: ${chosen.name}`); + + if (!existsSync(outDir)) await mkdir(outDir, { recursive: true }); + + const tmpFile = path.join(outDir, chosen.name); + await downloadWithProgress(chosen.browser_download_url, tmpFile); + + const { unlink } = await import("node:fs/promises"); + + if (chosen.name.endsWith(".zip")) + { + await extractZip(tmpFile, outDir); + await unlink(tmpFile); + } else + { + extractNative(tmpFile, outDir); + if (!chosen.name.endsWith(".AppImage")) + { + await unlink(tmpFile); + } + } + + // Save version so next run can skip + await writeVersionCache(outDir, version); + + log(`\nDone! Chromium ${version} extracted to: ${outDir}`); + + const binaryPath = getBinaryPath(outDir, version, platform, arch); + log(`Binary: ${binaryPath}`); +} + +main().catch((e) => err(String(e))); \ No newline at end of file diff --git a/scripts/download-nw.ts b/scripts/download-nw.ts new file mode 100644 index 0000000..7d318b6 --- /dev/null +++ b/scripts/download-nw.ts @@ -0,0 +1,54 @@ +import { ensureDir, remove } from "fs-extra"; +import StreamZip from "node-stream-zip"; +import { spawnSync } from "node:child_process"; +import fs from 'node:fs/promises'; + +const VERSION = "0.110.1"; + +const platformMap: Record = { + "win32": "win", + "darwin": "osx" +}; +const extMap: Record = { + "win32": "zip", + "linux": "tar.gz", + "darwin": "zip" +}; + +console.log("Removing old download"); +await remove('./bin/nw'); + +const downloadUrl = `https://dl.nwjs.io/v${VERSION}/nwjs-sdk-v${VERSION}-${platformMap[process.platform] ?? process.platform}-${process.arch}.${extMap[process.platform]}`; + +console.log("Starting NW download from", downloadUrl); +const response = await fetch(downloadUrl); +if (!response.ok) throw new Error(response.statusText); +const downlodPath = `./bin/nw.${extMap[process.platform]}`; +await ensureDir('./bin'); +await Bun.write(downlodPath, response); +console.log("Downloaded NW to", downlodPath); + +if (downlodPath.endsWith('.zip')) +{ + await extractZip(downlodPath, './bin'); +} +else if (downlodPath.endsWith(".tar.gz")) +{ + const result = spawnSync("tar", ["-xvf", downlodPath, "-C", './bin'], { stdio: "inherit" }); + if (result.status !== 0) console.error("tar extraction failed"); +} + +console.log('Renaming to nw'); +await fs.rename(`./bin/nwjs-sdk-v${VERSION}-${platformMap[process.platform] ?? process.platform}-${process.arch}`, './bin/nw'); +await fs.rm(downlodPath); + +async function extractZip (src: string, outDir: string) +{ + console.log(`Extracting zip -> ${outDir}`); + const zip = new StreamZip.async({ file: src }); + const entries = await zip.entries(); + const total = Object.keys(entries).length; + await zip.extract(null, outDir); + await zip.close(); + console.log(`Extracted ${total} files.`); +} \ No newline at end of file diff --git a/scripts/drizzle/es-de/0000_sparkling_banshee.sql b/scripts/drizzle/es-de/0000_sparkling_banshee.sql new file mode 100644 index 0000000..c86dd0e --- /dev/null +++ b/scripts/drizzle/es-de/0000_sparkling_banshee.sql @@ -0,0 +1,34 @@ +CREATE TABLE `commands` ( + `system` text, + `label` text, + `command` text NOT NULL, + FOREIGN KEY (`system`) REFERENCES `systems`(`name`) ON UPDATE cascade ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `emulators` ( + `name` text PRIMARY KEY NOT NULL, + `fullname` text, + `systempath` text DEFAULT (json_array()) NOT NULL, + `staticpath` text DEFAULT (json_array()) NOT NULL, + `corepath` text DEFAULT (json_array()) NOT NULL, + `androidpackage` text DEFAULT (json_array()) NOT NULL, + `winregistrypath` text DEFAULT (json_array()) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `emulators_name_unique` ON `emulators` (`name`);--> statement-breakpoint +CREATE TABLE `systemMappings` ( + `source` text, + `sourceSlug` text, + `sourceId` integer, + `system` text NOT NULL, + FOREIGN KEY (`system`) REFERENCES `systems`(`name`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `systems` ( + `name` text PRIMARY KEY NOT NULL, + `fullname` text, + `path` text, + `extension` text DEFAULT (json_array()) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `systems_name_unique` ON `systems` (`name`); \ No newline at end of file diff --git a/scripts/drizzle/es-de/meta/0000_snapshot.json b/scripts/drizzle/es-de/meta/0000_snapshot.json new file mode 100644 index 0000000..5c40c18 --- /dev/null +++ b/scripts/drizzle/es-de/meta/0000_snapshot.json @@ -0,0 +1,234 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b4ee710f-eaa5-4bbb-9e69-13d490c7142c", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "commands": { + "name": "commands", + "columns": { + "system": { + "name": "system", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "commands_system_systems_name_fk": { + "name": "commands_system_systems_name_fk", + "tableFrom": "commands", + "tableTo": "systems", + "columnsFrom": [ + "system" + ], + "columnsTo": [ + "name" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "emulators": { + "name": "emulators", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "systempath": { + "name": "systempath", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_array())" + }, + "staticpath": { + "name": "staticpath", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_array())" + }, + "corepath": { + "name": "corepath", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_array())" + }, + "androidpackage": { + "name": "androidpackage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_array())" + }, + "winregistrypath": { + "name": "winregistrypath", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_array())" + } + }, + "indexes": { + "emulators_name_unique": { + "name": "emulators_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "systemMappings": { + "name": "systemMappings", + "columns": { + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceSlug": { + "name": "sourceSlug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceId": { + "name": "sourceId", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "system": { + "name": "system", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "systemMappings_system_systems_name_fk": { + "name": "systemMappings_system_systems_name_fk", + "tableFrom": "systemMappings", + "tableTo": "systems", + "columnsFrom": [ + "system" + ], + "columnsTo": [ + "name" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "systems": { + "name": "systems", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extension": { + "name": "extension", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_array())" + } + }, + "indexes": { + "systems_name_unique": { + "name": "systems_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/scripts/drizzle/es-de/meta/_journal.json b/scripts/drizzle/es-de/meta/_journal.json new file mode 100644 index 0000000..65dbc00 --- /dev/null +++ b/scripts/drizzle/es-de/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1776039605377, + "tag": "0000_sparkling_banshee", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/scripts/generate-audio-sprites.ts b/scripts/generate-audio-sprites.ts new file mode 100644 index 0000000..24b65df --- /dev/null +++ b/scripts/generate-audio-sprites.ts @@ -0,0 +1,30 @@ +import audioSprite from 'audiosprite'; +import path from 'node:path'; +import { soundMap } from '../src/mainview/scripts/audio/audioConstants'; + +var allFiles = await Array.fromAsync(new Bun.Glob('*.{ogg,wav}').scan({ cwd: './src/sounds' })); +const files = Object.values(soundMap).map(v => +{ + const existingFile = allFiles.find(f => f.startsWith(v.key)); + if (!existingFile) throw new Error(`Could not find file for sound ${v.key}`); + const filePath = path.join(path.resolve('./src/sounds'), existingFile); + return filePath; +}); +console.log("Loaded", files.join(",")); + +await new Promise((resolve) => +{ + audioSprite(files, + { + output: path.resolve('./src/mainview/assets/sounds'), + path: path.resolve('./src/sounds'), + format: 'howler', + export: 'ogg' + }, + async function (err, obj: any) + { + if (err) return console.error(err); + delete obj.urls; + Bun.file('./src/mainview/assets/sounds.json').write(JSON.stringify(obj, null, 2)).then(r => resolve(true)); + }); +}); \ No newline at end of file diff --git a/scripts/generate-es-de-mapping.ts b/scripts/generate-es-de-mapping.ts index 3c0aa10..8e8fa74 100644 --- a/scripts/generate-es-de-mapping.ts +++ b/scripts/generate-es-de-mapping.ts @@ -6,8 +6,6 @@ import { Database } from "bun:sqlite"; import * as schema from '../src/bun/api/schema/emulators'; import { migrate } from "drizzle-orm/bun-sqlite/migrator"; import { drizzle } from "drizzle-orm/bun-sqlite"; -import path from 'node:path'; -import { ensureDir } from 'fs-extra'; /** get all latest supported romm platforms */ const rommPlatforms = await getSupportedPlatformsEndpointApiPlatformsSupportedGet({ baseUrl: "https://demo.romm.app" }); @@ -57,6 +55,7 @@ await Promise.all(platforms.map(async ([platform, arch]) => const emulators = $r('ruleList emulator').toArray().map(s => { const $emulator = $r(s); + const comment = $emulator.contents().toArray().find((node) => node.type === 'comment'); const $systempath = $emulator.find('rule[type=systempath] entry'); const $staticpath = $emulator.find('rule[type=staticpath] entry'); const $corepath = $emulator.find('rule[type=corepath] entry'); @@ -66,12 +65,14 @@ await Promise.all(platforms.map(async ([platform, arch]) => const emulatorName = $emulator.attr('name'); const emulator: typeof schema.emulators.$inferInsert = { name: emulatorName!, + fullname: comment?.data.trim(), systempath: $systempath.toArray().map(p => $r(p).text()), staticpath: $staticpath.toArray().map(p => $r(p).text()), corepath: $corepath.toArray().map(p => $r(p).text()), androidpackage: $androidpackage.toArray().map(p => $r(p).text()), winregistrypath: $winregistrypath.toArray().map(p => $r(p).text()), }; + return emulator; }); @@ -95,12 +96,18 @@ await Promise.all(platforms.map(async ([platform, arch]) => }); const rommMapping = rommPlatforms.data?.find(p => - p.slug === (customMappings as any)[name] || - p.slug === name || - p.igdb_slug === name || - p.hltb_slug === name || - p.moby_slug === name || - p.display_name === fullname + { + const custom = (customMappings as any)[name]; + if (Array.isArray(custom) && custom.some(m => m === p.slug)) + { + return true; + } + + return p.slug === custom || + p.slug === name || + p.igdb_slug === name || + p.display_name === fullname; + } ); const mappings: { @@ -143,6 +150,7 @@ await Promise.all(platforms.map(async ([platform, arch]) => commands, mappings }; + return system; })); diff --git a/scripts/generate-flatpak-sources.ts b/scripts/generate-flatpak-sources.ts index 6217e30..75c6084 100644 --- a/scripts/generate-flatpak-sources.ts +++ b/scripts/generate-flatpak-sources.ts @@ -1,6 +1,5 @@ import { $ } from "bun"; -const lockfile = Bun.argv[2] ?? "bun.lockb"; const output = Bun.argv[3] ?? ".config/flatpak/sources.gen.json"; const text = await $`bun ./bun.lockb --hash: 0000000000000000-0000000000000000-0000000000000000-0000000000000000`.text(); diff --git a/scripts/package-bun.ts b/scripts/package-bun.ts index 917c0c5..d358ce7 100644 --- a/scripts/package-bun.ts +++ b/scripts/package-bun.ts @@ -1,17 +1,24 @@ import fs from "node:fs/promises"; import path, { } from "node:path"; import os from "node:os"; +import app from '../package.json'; const system = getPlatform(); const buildSubDir = process.env.BUILD_DIR ?? `./build/${system.platform}`; const compileOption: Bun.CompileBuildOptions = { outfile: "gameflow", - execArgv: ['--windows-hide-console'], autoloadTsconfig: true, autoloadPackageJson: true, autoloadDotenv: true, autoloadBunfig: true, + windows: { + hideConsole: true, + icon: './src/mainview/public/favicon.ico', + title: app.displayName, + description: app.description, + version: app.version + }, }; if (process.env.TARGET) @@ -19,16 +26,37 @@ if (process.env.TARGET) compileOption.target = process.env.TARGET as any; } -let webviewLib = "libwebview.dll"; -if (process.platform === 'linux' && system.arch === 'x64') - webviewLib = "libwebview-x64.so"; -if (process.platform === 'linux' && system.arch === 'arm64') - webviewLib = "libwebview-arm64.so"; -if (process.platform === 'darwin') - webviewLib = "libwebview-arm64.dylib"; +let zip: string | undefined; +let zipPath: string = ''; +let zipNodePath: string | undefined; +let webviewLib: string | undefined; +switch (process.platform) +{ + case "win32": + zip = "7za.exe"; + zipNodePath = "win"; + webviewLib = `libwebview.dll`; + break; + case "linux": + zip = "7za"; + zipNodePath = 'linux'; + webviewLib = `libwebview-${system.arch}.so`; + break; + case "darwin": + zip = "7za"; + zipNodePath = 'mac'; + webviewLib = `libwebview-${system.arch}.dylib`; + break; +} +if (!webviewLib) throw new Error("Could not find webviewlib"); + +let webviewLibPath = '.'; if (process.env.APPIMAGE === "true") - webviewLib = `./usr/lib/${webviewLib}`; +{ + webviewLibPath = `./usr/lib`; + zipPath = './usr/bin'; +} await Bun.build({ entrypoints: ["./src/bun/index.ts", `./src/bun/webview/${system.platform}.ts`], @@ -38,7 +66,8 @@ await Bun.build({ root: './src/bun', define: { "process.env.IS_BINARY": "true", - "process.env.WEBVIEW_PATH": `./${webviewLib}`, + "process.env.WEBVIEW_PATH": `${webviewLibPath}/${webviewLib}`, + "process.env.ZIP7_PATH": `"${zip}"` }, minify: process.env.NODE_ENV !== 'development', sourcemap: process.env.NODE_ENV === 'development' ? 'inline' : "linked", @@ -62,12 +91,16 @@ await Bun.build({ } } }); - build.onEnd(async () => + build.onEnd(async (b) => { + await fs.cp('./dist', `${buildSubDir}/dist`, { recursive: true }); await fs.cp('./drizzle', `${buildSubDir}/drizzle`, { recursive: true }); await fs.cp(`./vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, `${buildSubDir}/vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, { recursive: true }); await fs.cp(path.join(`node_modules/webview-bun/build/`, webviewLib), path.join(buildSubDir, webviewLib)); + await fs.cp(`node_modules/7zip-bin/${zipNodePath}/${process.arch}`, buildSubDir, { recursive: true, errorOnExist: false }); + if (await fs.exists('bin/chromium')) + await fs.cp('bin/chromium', `${buildSubDir}/bin/chromium`, { recursive: true, errorOnExist: false }); }); }, }] diff --git a/scripts/romm/openapi.json b/scripts/romm/openapi.json index 86d3921..4214bca 100644 --- a/scripts/romm/openapi.json +++ b/scripts/romm/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"RomM API","version":"4.6.1"},"paths":{"/api/heartbeat":{"get":{"tags":["system"],"summary":"Heartbeat","description":"Endpoint to set the CSRF token in cache and return all the basic RomM config\n\nReturns:\n HeartbeatReturn: TypedDict structure with all the defined values in the HeartbeatReturn class.","operationId":"heartbeat_api_heartbeat_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatResponse"}}}}}}},"/api/heartbeat/metadata/{source}":{"get":{"tags":["system"],"summary":"Metadata Heartbeat","description":"Endpoint to return the heartbeat of the metadata sources","operationId":"metadata_heartbeat_api_heartbeat_metadata__source__get","parameters":[{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Metadata Heartbeat Api Heartbeat Metadata Source Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/setup/library":{"get":{"tags":["system"],"summary":"Get Setup Library Info","description":"Get library structure information for setup wizard.\n\nOnly accessible during initial setup (no admin users) or with authentication.\n\nReturns:\n - detected_structure: \"struct_a\" (roms/{platform}), \"struct_b\" ({platform}/roms), or None\n - existing_platforms: list of objects with fs_slug and rom_count\n - supported_platforms: list of all supported platforms with metadata","operationId":"get_setup_library_info_api_setup_library_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}]}},"/api/setup/platforms":{"post":{"tags":["system"],"summary":"Create Setup Platforms","description":"Create platform folders during setup wizard.\n\nOnly accessible during initial setup (no admin users) or with authentication.\n\nArgs:\n platform_slugs: List of platform fs_slugs to create\n\nReturns:\n - success: bool\n - created_count: number of platforms created\n - message: success or error message","operationId":"create_setup_platforms_api_setup_platforms_post","requestBody":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Platform Slugs"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}]}},"/api/login":{"post":{"tags":["auth"],"summary":"Login","description":"Session login endpoint\n\nArgs:\n request (Request): Fastapi Request object\n credentials: Defaults to Depends(HTTPBasic()).\n\nRaises:\n CredentialsException: Invalid credentials\n UserDisabledException: Auth is disabled","operationId":"login_api_login_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBasic":[]}]}},"/api/logout":{"post":{"tags":["auth"],"summary":"Logout","description":"Session logout endpoint\n\nArgs:\n request (Request): Fastapi Request object","operationId":"logout_api_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/token":{"post":{"tags":["auth"],"summary":"Token","description":"OAuth2 token endpoint\n\nArgs:\n form_data (Annotated[OAuth2RequestForm, Depends): Form Data with OAuth2 info\n\nRaises:\n HTTPException: Missing refresh token\n HTTPException: Invalid refresh token\n HTTPException: Missing username or password\n HTTPException: Invalid username or password\n HTTPException: Client credentials are not yet supported\n HTTPException: Invalid or unsupported grant type\n HTTPException: Insufficient scope\n\nReturns:\n TokenResponse: TypedDict with the new generated token info","operationId":"token_api_token_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_token_api_token_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/login/openid":{"get":{"tags":["auth"],"summary":"Login Via Openid","description":"OIDC login endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nRaises:\n OIDCDisabledException: OAuth is disabled\n OIDCNotConfiguredException: OAuth not configured\n\nReturns:\n RedirectResponse: Redirect to OIDC provider","operationId":"login_via_openid_api_login_openid_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/oauth/openid":{"get":{"tags":["auth"],"summary":"Auth Openid","description":"OIDC callback endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nRaises:\n OIDCDisabledException: OAuth is disabled\n OIDCNotConfiguredException: OAuth not configured\n AuthCredentialsException: Invalid credentials\n UserDisabledException: Auth is disabled\n\nReturns:\n RedirectResponse: Redirect to home page","operationId":"auth_openid_api_oauth_openid_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/forgot-password":{"post":{"tags":["auth"],"summary":"Request Password Reset","description":"Request a password reset link for the user.\n\nArgs:\n username (str): Username of the user requesting the reset\nReturns:\n None: Returns 200 OK status","operationId":"request_password_reset_api_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_request_password_reset_api_forgot_password_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reset-password":{"post":{"tags":["auth"],"summary":"Reset Password","description":"Reset password using the token.\n\nArgs:\n token (str): Reset token from the URL\n new_password (str): New user password\n\nReturns:\n None: Returns 200 OK status","operationId":"reset_password_api_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_reset_password_api_reset_password_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/users":{"get":{"tags":["users"],"summary":"Get Users","description":"Get all users endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[UserSchema]: All users stored in the RomM's database","operationId":"get_users_api_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/UserSchema"},"type":"array","title":"Response Get Users Api Users Get"}}}}},"security":[{"OAuth2PasswordBearer":["users.read"]},{"HTTPBasic":[]}]},"post":{"tags":["users"],"summary":"Add User","description":"Create user endpoint\n\nArgs:\n request (Request): Fastapi Requests object\n username (str): User username\n password (str): User password\n email (str): User email\n role (str): RomM Role object represented as string\n\nReturns:\n UserSchema: Newly created user","operationId":"add_user_api_users_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_add_user_api_users_post"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}]}},"/api/users/invite-link":{"post":{"tags":["users"],"summary":"Create Invite Link","description":"Create an invite link for a user.\n\nArgs:\n request (Request): FastAPI Request object\n role (str): The role of the user\n\nReturns:\n InviteLinkSchema: Invite link","operationId":"create_invite_link_api_users_invite_link_post","security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}],"parameters":[{"name":"role","in":"query","required":true,"schema":{"type":"string","title":"Role"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteLinkSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/users/register":{"post":{"tags":["users"],"summary":"Create User From Invite","description":"Create user endpoint with invite link\n\nArgs:\n username (str): User username\n email (str): User email\n password (str): User password\n token (str): Invite link token\n\nReturns:\n UserSchema: Newly created user","operationId":"create_user_from_invite_api_users_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_create_user_from_invite_api_users_register_post"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/users/me":{"get":{"tags":["users"],"summary":"Get Current User","description":"Get current user endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n UserSchema | None: Current user","operationId":"get_current_user_api_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/UserSchema"},{"type":"null"}],"title":"Response Get Current User Api Users Me Get"}}}}},"security":[{"OAuth2PasswordBearer":["me.read"]},{"HTTPBasic":[]}]}},"/api/users/{id}":{"get":{"tags":["users"],"summary":"Get User","description":"Get user endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n UserSchem: User stored in the RomM's database","operationId":"get_user_api_users__id__get","security":[{"OAuth2PasswordBearer":["users.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["users"],"summary":"Update User","description":"Update user endpoint\n\nArgs:\n request (Request): Fastapi Requests object\n user_id (int): User internal id\n form_data (Annotated[UserUpdateForm, Depends): Form Data with user updated info\n\nRaises:\n HTTPException: User is not found in database\n HTTPException: Username already in use by another user\n\nReturns:\n UserSchema: Updated user info","operationId":"update_user_api_users__id__put","security":[{"OAuth2PasswordBearer":["me.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/UserForm"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["users"],"summary":"Delete User","description":"Delete a user by ID.\n\nRaises:\n HTTPException: User is not found in database\n HTTPException: User deleting itself\n HTTPException: User is the last admin user","operationId":"delete_user_api_users__id__delete","security":[{"OAuth2PasswordBearer":["users.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"User internal id.","title":"Id"},"description":"User internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad Request"},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/users/{id}/ra/refresh":{"post":{"tags":["users"],"summary":"Refresh RetroAchievements","description":"Refresh RetroAchievements progression data for a user.","operationId":"refresh_retro_achievements_api_users__id__ra_refresh_post","security":[{"OAuth2PasswordBearer":["me.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"User internal id.","title":"Id"},"description":"User internal id."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_refresh_retro_achievements_api_users__id__ra_refresh_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platforms":{"post":{"tags":["platforms"],"summary":"Add Platform","description":"Create a platform.","operationId":"add_platform_api_platforms_post","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_add_platform_api_platforms_post"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["platforms"],"summary":"Get Platforms","description":"Retrieve platforms.","operationId":"get_platforms_api_platforms_get","security":[{"OAuth2PasswordBearer":["platforms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"updated_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter platforms updated after this datetime (ISO 8601 format with timezone information).","title":"Updated After"},"description":"Filter platforms updated after this datetime (ISO 8601 format with timezone information)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PlatformSchema"},"title":"Response Get Platforms Api Platforms Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platforms/supported":{"get":{"tags":["platforms"],"summary":"Get Supported Platforms Endpoint","description":"Retrieve the list of supported platforms.","operationId":"get_supported_platforms_endpoint_api_platforms_supported_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PlatformSchema"},"type":"array","title":"Response Get Supported Platforms Endpoint Api Platforms Supported Get"}}}}},"security":[{"OAuth2PasswordBearer":["platforms.read"]},{"HTTPBasic":[]}]}},"/api/platforms/{id}":{"get":{"tags":["platforms"],"summary":"Get Platform","description":"Retrieve a platform by ID.","operationId":"get_platform_api_platforms__id__get","security":[{"OAuth2PasswordBearer":["platforms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Platform id.","title":"Id"},"description":"Platform id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["platforms"],"summary":"Update Platform","description":"Update a platform.","operationId":"update_platform_api_platforms__id__put","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Platform id.","title":"Id"},"description":"Platform id."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_update_platform_api_platforms__id__put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["platforms"],"summary":"Delete Platform","description":"Delete a platform by ID.","operationId":"delete_platform_api_platforms__id__delete","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Platform id.","title":"Id"},"description":"Platform id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms":{"post":{"tags":["roms"],"summary":"Add Rom","description":"Upload a single rom.","operationId":"add_rom_api_roms_post","security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"x-upload-platform","in":"header","required":true,"schema":{"type":"integer","minimum":1,"description":"Platform internal id.","title":"X-Upload-Platform"},"description":"Platform internal id."},{"name":"x-upload-filename","in":"header","required":true,"schema":{"type":"string","description":"The name of the file being uploaded.","title":"X-Upload-Filename"},"description":"The name of the file being uploaded."}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad Request"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["roms"],"summary":"Get Roms","description":"Retrieve roms.","operationId":"get_roms_api_roms_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"with_char_index","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to get the char index.","default":true,"title":"With Char Index"},"description":"Whether to get the char index."},{"name":"with_filter_values","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to return filter values.","default":true,"title":"With Filter Values"},"description":"Whether to return filter values."},{"name":"search_term","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search term to filter roms.","title":"Search Term"},"description":"Search term to filter roms."},{"name":"platform_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"integer"}},{"type":"null"}],"description":"Platform internal ids. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Platform Ids"},"description":"Platform internal ids. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"collection_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Collection internal id.","title":"Collection Id"},"description":"Collection internal id."},{"name":"virtual_collection_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Virtual collection internal id.","title":"Virtual Collection Id"},"description":"Virtual collection internal id."},{"name":"smart_collection_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Smart collection internal id.","title":"Smart Collection Id"},"description":"Smart collection internal id."},{"name":"matched","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom matched at least one metadata source.","title":"Matched"},"description":"Whether the rom matched at least one metadata source."},{"name":"favorite","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is marked as favorite.","title":"Favorite"},"description":"Whether the rom is marked as favorite."},{"name":"duplicate","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is marked as duplicate.","title":"Duplicate"},"description":"Whether the rom is marked as duplicate."},{"name":"last_played","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom has a last played value for the current user.","title":"Last Played"},"description":"Whether the rom has a last played value for the current user."},{"name":"playable","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is playable from the browser.","title":"Playable"},"description":"Whether the rom is playable from the browser."},{"name":"missing","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is missing from the filesystem.","title":"Missing"},"description":"Whether the rom is missing from the filesystem."},{"name":"has_ra","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom has RetroAchievements data.","title":"Has Ra"},"description":"Whether the rom has RetroAchievements data."},{"name":"verified","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is verified by Hasheous.","title":"Verified"},"description":"Whether the rom is verified by Hasheous."},{"name":"group_by_meta_id","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to group roms by metadata ID (IGDB / Moby / ScreenScraper / RetroAchievements / LaunchBox).","default":false,"title":"Group By Meta Id"},"description":"Whether to group roms by metadata ID (IGDB / Moby / ScreenScraper / RetroAchievements / LaunchBox)."},{"name":"genres","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated genre. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Genres"},"description":"Associated genre. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"franchises","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated franchise. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Franchises"},"description":"Associated franchise. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"collections","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated collection. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Collections"},"description":"Associated collection. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"companies","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated company. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Companies"},"description":"Associated company. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"age_ratings","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated age rating. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Age Ratings"},"description":"Associated age rating. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"statuses","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Game status, set by the current user. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Statuses"},"description":"Game status, set by the current user. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"regions","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated region tag. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Regions"},"description":"Associated region tag. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"languages","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated language tag. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Languages"},"description":"Associated language tag. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"player_counts","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated player count. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Player Counts"},"description":"Associated player count. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"genres_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for genres filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Genres Logic"},"description":"Logic operator for genres filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"franchises_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for franchises filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Franchises Logic"},"description":"Logic operator for franchises filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"collections_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for collections filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Collections Logic"},"description":"Logic operator for collections filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"companies_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for companies filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Companies Logic"},"description":"Logic operator for companies filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"age_ratings_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for age ratings filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Age Ratings Logic"},"description":"Logic operator for age ratings filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"regions_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for regions filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Regions Logic"},"description":"Logic operator for regions filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"languages_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for languages filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Languages Logic"},"description":"Logic operator for languages filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"statuses_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for statuses filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Statuses Logic"},"description":"Logic operator for statuses filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"player_counts_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for player counts filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Player Counts Logic"},"description":"Logic operator for player counts filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"order_by","in":"query","required":false,"schema":{"type":"string","description":"Field to order results by.","default":"name","title":"Order By"},"description":"Field to order results by."},{"name":"order_dir","in":"query","required":false,"schema":{"type":"string","description":"Order direction, either 'asc' or 'desc'.","default":"asc","title":"Order Dir"},"description":"Order direction, either 'asc' or 'desc'."},{"name":"updated_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter roms updated after this datetime (ISO 8601 format with timezone information).","title":"Updated After"},"description":"Filter roms updated after this datetime (ISO 8601 format with timezone information)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Page size limit","default":50,"title":"Limit"},"description":"Page size limit"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Page offset","default":0,"title":"Offset"},"description":"Page offset"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomLimitOffsetPage_SimpleRomSchema_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/download":{"get":{"tags":["roms"],"summary":"Download Roms","description":"Download a list of roms as a zip file.","operationId":"download_roms_api_roms_download_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_ids","in":"query","required":true,"schema":{"type":"string","description":"Comma-separated list of ROM IDs to download as a zip file.","title":"Rom Ids"},"description":"Comma-separated list of ROM IDs to download as a zip file."},{"name":"filename","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Name for the zip file (optional).","title":"Filename"},"description":"Name for the zip file (optional)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/by-metadata-provider":{"get":{"tags":["roms"],"summary":"Get Rom By Metadata Provider","description":"Retrieve a rom by metadata ID.","operationId":"get_rom_by_metadata_provider_api_roms_by_metadata_provider_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"igdb_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"IGDB ID to search by","title":"Igdb Id"},"description":"IGDB ID to search by"},{"name":"moby_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"MobyGames ID to search by","title":"Moby Id"},"description":"MobyGames ID to search by"},{"name":"ss_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"ScreenScraper ID to search by","title":"Ss Id"},"description":"ScreenScraper ID to search by"},{"name":"ra_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"RetroAchievements ID to search by","title":"Ra Id"},"description":"RetroAchievements ID to search by"},{"name":"launchbox_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"LaunchBox ID to search by","title":"Launchbox Id"},"description":"LaunchBox ID to search by"},{"name":"hasheous_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Hasheous ID to search by","title":"Hasheous Id"},"description":"Hasheous ID to search by"},{"name":"tgdb_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"TGDB ID to search by","title":"Tgdb Id"},"description":"TGDB ID to search by"},{"name":"flashpoint_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Flashpoint ID to search by","title":"Flashpoint Id"},"description":"Flashpoint ID to search by"},{"name":"hltb_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"HLTB ID to search by","title":"Hltb Id"},"description":"HLTB ID to search by"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailedRomSchema"}}}},"404":{"description":"Not Found"},"400":{"description":"Bad Request"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/by-hash":{"get":{"tags":["roms"],"summary":"Get Rom By Hash","operationId":"get_rom_by_hash_api_roms_by_hash_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"crc_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CRC hash value","title":"Crc Hash"},"description":"CRC hash value"},{"name":"md5_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"MD5 hash value","title":"Md5 Hash"},"description":"MD5 hash value"},{"name":"sha1_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"SHA1 hash value","title":"Sha1 Hash"},"description":"SHA1 hash value"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailedRomSchema"}}}},"404":{"description":"Not Found"},"400":{"description":"Bad Request"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/filters":{"get":{"tags":["roms"],"summary":"Get Rom Filters","operationId":"get_rom_filters_api_roms_filters_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RomFiltersDict"}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/roms/{id}":{"get":{"tags":["roms"],"summary":"Get Rom","description":"Retrieve a rom by ID.","operationId":"get_rom_api_roms__id__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailedRomSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["roms"],"summary":"Update Rom","description":"Update a rom.","operationId":"update_rom_api_roms__id__put","security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"remove_cover","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to remove the cover image for this rom.","default":false,"title":"Remove Cover"},"description":"Whether to remove the cover image for this rom."},{"name":"unmatch_metadata","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to remove the metadata matches for this game.","default":false,"title":"Unmatch Metadata"},"description":"Whether to remove the metadata matches for this game."}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_update_rom_api_roms__id__put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailedRomSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/content/{file_name}":{"head":{"tags":["roms"],"summary":"Head Rom Content","description":"Retrieve head information for a rom file download.","operationId":"head_rom_content_api_roms__id__content__file_name__head","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","description":"File name to download","title":"File Name"},"description":"File name to download"},{"name":"file_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated list of file ids to download for multi-part roms.","title":"File Ids"},"description":"Comma-separated list of file ids to download for multi-part roms."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["roms"],"summary":"Get Rom Content","description":"Download a rom.\n\nThis endpoint serves the content of the requested rom, as:\n- A single file for single file roms.\n- A zipped file for multi-part roms, including a .m3u file if applicable.","operationId":"get_rom_content_api_roms__id__content__file_name__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","description":"Zip file output name","title":"File Name"},"description":"Zip file output name"},{"name":"file_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated list of file ids to download for multi-part roms.","title":"File Ids"},"description":"Comma-separated list of file ids to download for multi-part roms."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/manuals":{"post":{"tags":["roms"],"summary":"Add Rom Manuals","description":"Upload manuals for a rom.","operationId":"add_rom_manuals_api_roms__id__manuals_post","security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"x-upload-filename","in":"header","required":true,"schema":{"type":"string","description":"The name of the file being uploaded.","title":"X-Upload-Filename"},"description":"The name of the file being uploaded."}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["roms"],"summary":"Delete Rom Manuals","description":"Delete manuals for a rom.","operationId":"delete_rom_manuals_api_roms__id__manuals_delete","security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/delete":{"post":{"tags":["roms"],"summary":"Delete Roms","description":"Delete roms.","operationId":"delete_roms_api_roms_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_delete_roms_api_roms_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkOperationResponse"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}]}},"/api/roms/{id}/props":{"put":{"tags":["roms"],"summary":"Update Rom User","description":"Update rom data associated to the current user.","operationId":"update_rom_user_api_roms__id__props_put","security":[{"OAuth2PasswordBearer":["roms.user.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_update_rom_user_api_roms__id__props_put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RomUserSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/files/{id}":{"get":{"tags":["roms"],"summary":"Get Romfile","description":"Retrieve a rom file by ID.","operationId":"get_romfile_api_roms_files__id__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom file internal id.","title":"Id"},"description":"Rom file internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RomFileSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/romsfiles/{id}/content/{file_name}":{"get":{"tags":["roms"],"summary":"Get Romfile Content","description":"Download a rom file.","operationId":"get_romfile_content_api_romsfiles__id__content__file_name__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom file internal id.","title":"Id"},"description":"Rom file internal id."},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","description":"File name to download","title":"File Name"},"description":"File name to download"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/notes":{"get":{"tags":["roms"],"summary":"Get Rom Notes","description":"Get all notes for a ROM.","operationId":"get_rom_notes_api_roms__id__notes_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"public_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return public notes","default":false,"title":"Public Only"},"description":"Only return public notes"},{"name":"search","in":"query","required":false,"schema":{"type":"string","description":"Search notes by title or content","title":"Search"},"description":"Search notes by title or content"},{"name":"tags","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Filter by tags","title":"Tags"},"description":"Filter by tags"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserNoteSchema"},"title":"Response Get Rom Notes Api Roms Id Notes Get"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["roms"],"summary":"Create Rom Note","description":"Create a new note for a ROM.","operationId":"create_rom_note_api_roms__id__notes_post","security":[{"OAuth2PasswordBearer":["roms.user.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Note Data"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserNoteSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/notes/{note_id}":{"put":{"tags":["roms"],"summary":"Update Rom Note","description":"Update a ROM note.","operationId":"update_rom_note_api_roms__id__notes__note_id__put","security":[{"OAuth2PasswordBearer":["roms.user.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"note_id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Note id.","title":"Note Id"},"description":"Note id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Note Data"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserNoteSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["roms"],"summary":"Delete Rom Note","description":"Delete a ROM note.","operationId":"delete_rom_note_api_roms__id__notes__note_id__delete","security":[{"OAuth2PasswordBearer":["roms.user.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"note_id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Note id.","title":"Note Id"},"description":"Note id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Rom Note Api Roms Id Notes Note Id Delete"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/search/roms":{"get":{"tags":["search"],"summary":"Search Rom","description":"Search for rom in metadata providers\n\nArgs:\n request (Request): FastAPI request\n rom_id (int): Rom ID\n source (str): Source of the rom\n search_term (str, optional): Search term. Defaults to None.\n search_by (str, optional): Search by name or ID. Defaults to \"name\".\n search_extended (bool, optional): Search extended info. Defaults to False.\n\nReturns:\n list[SearchRomSchema]: List of matched roms","operationId":"search_rom_api_search_roms_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}},{"name":"search_term","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search Term"}},{"name":"search_by","in":"query","required":false,"schema":{"type":"string","default":"name","title":"Search By"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchRomSchema"},"title":"Response Search Rom Api Search Roms Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/search/cover":{"get":{"tags":["search"],"summary":"Search Cover","operationId":"search_cover_api_search_cover_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"search_term","in":"query","required":false,"schema":{"type":"string","default":"","title":"Search Term"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchCoverSchema"},"title":"Response Search Cover Api Search Cover Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves":{"post":{"tags":["saves"],"summary":"Add Save","operationId":"add_save_api_saves_post","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}},{"name":"emulator","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emulator"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["saves"],"summary":"Get Saves","operationId":"get_saves_api_saves_get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rom Id"}},{"name":"platform_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Platform Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SaveSchema"},"title":"Response Get Saves Api Saves Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves/{id}":{"get":{"tags":["saves"],"summary":"Get Save","operationId":"get_save_api_saves__id__get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["saves"],"summary":"Update Save","operationId":"update_save_api_saves__id__put","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves/delete":{"post":{"tags":["saves"],"summary":"Delete Saves","description":"Delete saves.","operationId":"delete_saves_api_saves_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_delete_saves_api_saves_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Delete Saves Api Saves Delete Post"}}}},"400":{"description":"Bad Request"},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}]}},"/api/states":{"post":{"tags":["states"],"summary":"Add State","operationId":"add_state_api_states_post","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}},{"name":"emulator","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emulator"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StateSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["states"],"summary":"Get States","operationId":"get_states_api_states_get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rom Id"}},{"name":"platform_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Platform Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StateSchema"},"title":"Response Get States Api States Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/states/{id}":{"get":{"tags":["states"],"summary":"Get State","operationId":"get_state_api_states__id__get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StateSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["states"],"summary":"Update State","operationId":"update_state_api_states__id__put","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StateSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/states/delete":{"post":{"tags":["states"],"summary":"Delete States","description":"Delete states.","operationId":"delete_states_api_states_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_delete_states_api_states_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Delete States Api States Delete Post"}}}},"400":{"description":"Bad Request"},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}]}},"/api/tasks":{"get":{"tags":["tasks"],"summary":"List Tasks","description":"List all available tasks grouped by task type.\n\nArgs:\n request (Request): FastAPI Request object\nReturns:\n GroupedTasksDict: Dictionary with tasks grouped by their type (scheduled, manual, watcher)","operationId":"list_tasks_api_tasks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"items":{"$ref":"#/components/schemas/TaskInfo"},"type":"array"},"type":"object","title":"Response List Tasks Api Tasks Get"}}}}},"security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}]}},"/api/tasks/status":{"get":{"tags":["tasks"],"summary":"Get Tasks Status","description":"Get all active, queued, completed, and failed tasks.\n\nArgs:\n request (Request): FastAPI Request object\nReturns:\n list[TaskStatusResponse]: List of all tasks with their current status","operationId":"get_tasks_status_api_tasks_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"anyOf":[{"$ref":"#/components/schemas/ScanTaskStatusResponse"},{"$ref":"#/components/schemas/ConversionTaskStatusResponse"},{"$ref":"#/components/schemas/UpdateTaskStatusResponse"},{"$ref":"#/components/schemas/CleanupTaskStatusResponse"},{"$ref":"#/components/schemas/WatcherTaskStatusResponse"},{"$ref":"#/components/schemas/GenericTaskStatusResponse"}]},"type":"array","title":"Response Get Tasks Status Api Tasks Status Get"}}}}},"security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}]}},"/api/tasks/{task_id}":{"get":{"tags":["tasks"],"summary":"Get Task By Id","description":"Get the status of a task by its job ID.\n\nArgs:\n request (Request): FastAPI Request object\n task_id (str): Job ID of the task to retrieve status for\nReturns:\n TaskStatusResponse: Task status information","operationId":"get_task_by_id_api_tasks__task_id__get","security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}],"parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ScanTaskStatusResponse"},{"$ref":"#/components/schemas/ConversionTaskStatusResponse"},{"$ref":"#/components/schemas/UpdateTaskStatusResponse"},{"$ref":"#/components/schemas/CleanupTaskStatusResponse"},{"$ref":"#/components/schemas/WatcherTaskStatusResponse"},{"$ref":"#/components/schemas/GenericTaskStatusResponse"}],"title":"Response Get Task By Id Api Tasks Task Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks/run":{"post":{"tags":["tasks"],"summary":"Run All Tasks","description":"Run all runnable tasks endpoint\n\nArgs:\n request (Request): FastAPI Request object\nReturns:\n TaskExecutionResponse: Task execution response with details","operationId":"run_all_tasks_api_tasks_run_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TaskExecutionResponse"},"type":"array","title":"Response Run All Tasks Api Tasks Run Post"}}}}},"security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}]}},"/api/tasks/run/{task_name}":{"post":{"tags":["tasks"],"summary":"Run Single Task","description":"Run a single task endpoint.\n\nArgs:\n request (Request): FastAPI Request object\n task_name (str): Name of the task to run\nReturns:\n TaskExecutionResponse: Task execution response with details","operationId":"run_single_task_api_tasks_run__task_name__post","security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}],"parameters":[{"name":"task_name","in":"path","required":true,"schema":{"type":"string","title":"Task Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskExecutionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/webrcade":{"get":{"tags":["feeds"],"summary":"Platforms Webrcade Feed","description":"Get webrcade feed endpoint\nhttps://docs.webrcade.com/feeds/format/\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n WebrcadeFeedSchema: Webrcade feed object schema","operationId":"platforms_webrcade_feed_api_feeds_webrcade_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebrcadeFeedSchema"}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/feeds/tinfoil":{"get":{"tags":["feeds"],"summary":"Tinfoil Index Feed","description":"Get tinfoil custom index feed endpoint\nhttps://blawar.github.io/tinfoil/custom_index/\n\nArgs:\n request (Request): Fastapi Request object\n slug (str, optional): Platform slug. Defaults to \"switch\".\n\nReturns:\n TinfoilFeedSchema: Tinfoil feed object schema","operationId":"tinfoil_index_feed_api_feeds_tinfoil_get","security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}],"parameters":[{"name":"slug","in":"query","required":false,"schema":{"type":"string","default":"switch","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TinfoilFeedSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/pkgi/ps3/{content_type}":{"get":{"tags":["feeds"],"summary":"Pkgi Ps3 Feed","description":"Get PKGi PS3 feed endpoint\nhttps://github.com/bucanero/pkgi-ps3\n\nArgs:\n request (Request): Fastapi Request object\n content_type (str): Content type (game, dlc, demo, update, patch, mod, translation, prototype)\n\nReturns:\n Response: txt file with PKGi PS3 database format","operationId":"pkgi_ps3_feed_api_feeds_pkgi_ps3__content_type__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"content_type","in":"path","required":true,"schema":{"type":"string","description":"Content type","title":"Content Type"},"description":"Content type"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/pkgi/psvita/{content_type}":{"get":{"tags":["feeds"],"summary":"Pkgi Psvita Feed","description":"Get PKGi PS Vita feed endpoint\nhttps://github.com/mmozeiko/pkgi\n\nArgs:\n request (Request): Fastapi Request object\n content_type (str): Content type (game, dlc, demo, update, patch, mod, translation, prototype)\n\nReturns:\n Response: txt file with PKGi PS Vita database format","operationId":"pkgi_psvita_feed_api_feeds_pkgi_psvita__content_type__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"content_type","in":"path","required":true,"schema":{"type":"string","description":"Content type","title":"Content Type"},"description":"Content type"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/pkgi/psp/{content_type}":{"get":{"tags":["feeds"],"summary":"Pkgi Psp Feed","description":"Get PKGi PSP feed endpoint\nhttps://github.com/bucanero/pkgi-psp\n\nArgs:\n request (Request): Fastapi Request object\n content_type (str): Content type (game, dlc, demo, update, patch, mod, translation, prototype)\n\nReturns:\n Response: txt file with PKGi PSP database format","operationId":"pkgi_psp_feed_api_feeds_pkgi_psp__content_type__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"content_type","in":"path","required":true,"schema":{"type":"string","description":"Content type","title":"Content Type"},"description":"Content type"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/fpkgi/{platform_slug}":{"get":{"tags":["feeds"],"summary":"Fpkgi Feed","description":"https://github.com/ItsJokerZz/FPKGi\n\nArgs:\n request (Request): Fastapi Request object\n platform_slug (str): Platform slug (ps4, ps5)\n\nReturns:\n Response: JSON file in FPKGi format","operationId":"fpkgi_feed_api_feeds_fpkgi__platform_slug__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_slug","in":"path","required":true,"schema":{"type":"string","title":"Platform Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/kekatsu/{platform_slug}":{"get":{"tags":["feeds"],"summary":"Kekatsu Ds Feed","description":"Get Kekatsu DS feed endpoint\nhttps://github.com/cavv-dev/Kekatsu-DS\n\nArgs:\n request (Request): Fastapi Request object\n platform_slug (str): Platform slug (nds, nintendo-ds, ds, gba, etc.)\n\nReturns:\n Response: Text file with Kekatsu DS database format","operationId":"kekatsu_ds_feed_api_feeds_kekatsu__platform_slug__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_slug","in":"path","required":true,"schema":{"type":"string","title":"Platform Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/config":{"get":{"tags":["config"],"summary":"Get Config","description":"Get config endpoint\n\nReturns:\n ConfigResponse: RomM's configuration","operationId":"get_config_api_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigResponse"}}}}}}},"/api/config/system/platforms":{"post":{"tags":["config"],"summary":"Add Platform Binding","description":"Add platform binding to the configuration","operationId":"add_platform_binding_api_config_system_platforms_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}]}},"/api/config/system/platforms/{fs_slug}":{"delete":{"tags":["config"],"summary":"Delete Platform Binding","description":"Delete platform binding from the configuration","operationId":"delete_platform_binding_api_config_system_platforms__fs_slug__delete","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"fs_slug","in":"path","required":true,"schema":{"type":"string","title":"Fs Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/config/system/versions":{"post":{"tags":["config"],"summary":"Add Platform Version","description":"Add platform version to the configuration","operationId":"add_platform_version_api_config_system_versions_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}]}},"/api/config/system/versions/{fs_slug}":{"delete":{"tags":["config"],"summary":"Delete Platform Version","description":"Delete platform version from the configuration","operationId":"delete_platform_version_api_config_system_versions__fs_slug__delete","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"fs_slug","in":"path","required":true,"schema":{"type":"string","title":"Fs Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/config/exclude":{"post":{"tags":["config"],"summary":"Add Exclusion","description":"Add platform exclusion to the configuration","operationId":"add_exclusion_api_config_exclude_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}]}},"/api/config/exclude/{exclusion_type}/{exclusion_value}":{"delete":{"tags":["config"],"summary":"Delete Exclusion","description":"Delete platform binding from the configuration","operationId":"delete_exclusion_api_config_exclude__exclusion_type___exclusion_value__delete","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"exclusion_type","in":"path","required":true,"schema":{"type":"string","title":"Exclusion Type"}},{"name":"exclusion_value","in":"path","required":true,"schema":{"type":"string","title":"Exclusion Value"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats":{"get":{"tags":["stats"],"summary":"Stats","description":"Endpoint to return the current RomM stats\n\nReturns:\n dict: Dictionary with all the stats","operationId":"stats_api_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsReturn"}}}}}}},"/api/raw/assets/{path}":{"head":{"tags":["raw"],"summary":"Head Raw Asset","operationId":"head_raw_asset_api_raw_assets__path__head","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["raw"],"summary":"Get Raw Asset","description":"Download a single asset file\n\nArgs:\n request (Request): Fastapi Request object\n path (str): Relative path to the asset file\n\nReturns:\n FileResponse: Returns a single asset file\n\nRaises:\n HTTPException: 404 if asset not found or access denied","operationId":"get_raw_asset_api_raw_assets__path__get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/screenshots":{"post":{"tags":["screenshots"],"summary":"Add Screenshot","operationId":"add_screenshot_api_screenshots_post","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScreenshotSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/firmware":{"post":{"tags":["firmware"],"summary":"Add Firmware","description":"Upload firmware files endpoint\n\nArgs:\n request (Request): Fastapi Request object\n platform_slug (str): Slug of the platform where to upload the files\n files (list[UploadFile], optional): List of files to upload\n\nRaises:\n HTTPException\n\nReturns:\n AddFirmwareResponse: Standard message response","operationId":"add_firmware_api_firmware_post","security":[{"OAuth2PasswordBearer":["firmware.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_id","in":"query","required":true,"schema":{"type":"integer","title":"Platform Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_add_firmware_api_firmware_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddFirmwareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["firmware"],"summary":"Get Platform Firmware","description":"Get firmware endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[FirmwareSchema]: Firmware stored in the database","operationId":"get_platform_firmware_api_firmware_get","security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Platform Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FirmwareSchema"},"title":"Response Get Platform Firmware Api Firmware Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/firmware/{id}":{"get":{"tags":["firmware"],"summary":"Get Firmware","description":"Get firmware endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Firmware internal id\n\nReturns:\n FirmwareSchema: Firmware stored in the database","operationId":"get_firmware_api_firmware__id__get","security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmwareSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/firmware/{id}/content/{file_name}":{"head":{"tags":["firmware"],"summary":"Head Firmware Content","description":"Head firmware content endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Rom internal id\n file_name (str): Required due to a bug in emulatorjs\n\nReturns:\n FileResponse: Returns the response with headers","operationId":"head_firmware_content_api_firmware__id__content__file_name__head","security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","title":"File Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["firmware"],"summary":"Get Firmware Content","description":"Download firmware endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Rom internal id\n file_name (str): Required due to a bug in emulatorjs\n\nReturns:\n FileResponse: Returns the firmware file","operationId":"get_firmware_content_api_firmware__id__content__file_name__get","security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","title":"File Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/firmware/delete":{"post":{"tags":["firmware"],"summary":"Delete Firmware","description":"Delete firmware.","operationId":"delete_firmware_api_firmware_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_delete_firmware_api_firmware_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkOperationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["firmware.write"]},{"HTTPBasic":[]}]}},"/api/collections":{"post":{"tags":["collections"],"summary":"Add Collection","description":"Create collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n CollectionSchema: Just created collection","operationId":"add_collection_api_collections_post","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"is_public","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}},{"name":"is_favorite","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Favorite"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_add_collection_api_collections_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["collections"],"summary":"Get Collections","description":"Get collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n updated_after: Filter collections updated after this datetime\n\nReturns:\n list[CollectionSchema]: List of collections","operationId":"get_collections_api_collections_get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"updated_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter collections updated after this datetime (ISO 8601 format with timezone information).","title":"Updated After"},"description":"Filter collections updated after this datetime (ISO 8601 format with timezone information)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CollectionSchema"},"title":"Response Get Collections Api Collections Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/smart":{"post":{"tags":["collections"],"summary":"Add Smart Collection","description":"Create smart collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n SmartCollectionSchema: Just created smart collection","operationId":"add_smart_collection_api_collections_smart_post","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"is_public","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartCollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["collections"],"summary":"Get Smart Collections","description":"Get smart collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n updated_after: Filter smart collections updated after this datetime\n\nReturns:\n list[SmartCollectionSchema]: List of smart collections","operationId":"get_smart_collections_api_collections_smart_get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"updated_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter smart collections updated after this datetime (ISO 8601 format with timezone information).","title":"Updated After"},"description":"Filter smart collections updated after this datetime (ISO 8601 format with timezone information)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SmartCollectionSchema"},"title":"Response Get Smart Collections Api Collections Smart Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/virtual":{"get":{"tags":["collections"],"summary":"Get Virtual Collections","description":"Get virtual collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[VirtualCollectionSchema]: List of virtual collections","operationId":"get_virtual_collections_api_collections_virtual_get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"type","in":"query","required":true,"schema":{"type":"string","title":"Type"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VirtualCollectionSchema"},"title":"Response Get Virtual Collections Api Collections Virtual Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/{id}":{"get":{"tags":["collections"],"summary":"Get Collection","description":"Get collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int, optional): Collection id. Defaults to None.\n\nReturns:\n CollectionSchema: Collection","operationId":"get_collection_api_collections__id__get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["collections"],"summary":"Update Collection","description":"Update collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n CollectionSchema: Updated collection","operationId":"update_collection_api_collections__id__put","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"remove_cover","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Remove Cover"}},{"name":"is_public","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_update_collection_api_collections__id__put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["collections"],"summary":"Delete Collection","description":"Delete a collection by ID.","operationId":"delete_collection_api_collections__id__delete","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Collection internal id.","title":"Id"},"description":"Collection internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/virtual/{id}":{"get":{"tags":["collections"],"summary":"Get Virtual Collection","description":"Get virtual collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (str): Virtual collection id\n\nReturns:\n VirtualCollectionSchema: Virtual collection","operationId":"get_virtual_collection_api_collections_virtual__id__get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualCollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/smart/{id}":{"get":{"tags":["collections"],"summary":"Get Smart Collection","description":"Get smart collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Smart collection id\n\nReturns:\n SmartCollectionSchema: Smart collection","operationId":"get_smart_collection_api_collections_smart__id__get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartCollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["collections"],"summary":"Update Smart Collection","description":"Update smart collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Smart collection id\n\nReturns:\n SmartCollectionSchema: Updated smart collection","operationId":"update_smart_collection_api_collections_smart__id__put","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"is_public","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartCollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["collections"],"summary":"Delete Smart Collection","description":"Delete a smart collection by ID.","operationId":"delete_smart_collection_api_collections_smart__id__delete","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Smart collection internal id.","title":"Id"},"description":"Smart collection internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/gamelist/export":{"post":{"tags":["gamelist"],"summary":"Export Gamelist","description":"Export platforms/ROMs to gamelist.xml format and write to platform directories","operationId":"export_gamelist_api_gamelist_export_post","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_ids","in":"query","required":true,"schema":{"type":"array","items":{"type":"integer"},"description":"List of platform IDs to export","title":"Platform Ids"},"description":"List of platform IDs to export"},{"name":"local_export","in":"query","required":false,"schema":{"type":"boolean","description":"Use local paths instead of URLs","default":false,"title":"Local Export"},"description":"Use local paths instead of URLs"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/netplay/list":{"get":{"tags":["netplay"],"summary":"Get Rooms","operationId":"get_rooms_api_netplay_list_get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"game_id","in":"query","required":true,"schema":{"type":"string","title":"Game Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/RoomsResponse"},"title":"Response Get Rooms Api Netplay List Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AddFirmwareResponse":{"properties":{"uploaded":{"type":"integer","title":"Uploaded"},"firmware":{"items":{"$ref":"#/components/schemas/FirmwareSchema"},"type":"array","title":"Firmware"}},"type":"object","required":["uploaded","firmware"],"title":"AddFirmwareResponse"},"Body_add_collection_api_collections_post":{"properties":{"artwork":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Artwork"}},"type":"object","title":"Body_add_collection_api_collections_post"},"Body_add_firmware_api_firmware_post":{"properties":{"files":{"items":{"type":"string","format":"binary"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_add_firmware_api_firmware_post"},"Body_add_platform_api_platforms_post":{"properties":{"fs_slug":{"type":"string","title":"Fs Slug","description":"Platform slug."}},"type":"object","required":["fs_slug"],"title":"Body_add_platform_api_platforms_post"},"Body_add_user_api_users_post":{"properties":{"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"role":{"type":"string","title":"Role"}},"type":"object","required":["username","email","password","role"],"title":"Body_add_user_api_users_post"},"Body_create_user_from_invite_api_users_register_post":{"properties":{"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"token":{"type":"string","title":"Token"}},"type":"object","required":["username","email","password","token"],"title":"Body_create_user_from_invite_api_users_register_post"},"Body_delete_firmware_api_firmware_delete_post":{"properties":{"firmware":{"items":{"type":"integer"},"type":"array","title":"Firmware","description":"List of firmware ids to delete from database."},"delete_from_fs":{"items":{"type":"integer"},"type":"array","title":"Delete From Fs","description":"List of firmware ids to delete from filesystem."}},"type":"object","required":["firmware"],"title":"Body_delete_firmware_api_firmware_delete_post"},"Body_delete_roms_api_roms_delete_post":{"properties":{"roms":{"items":{"type":"integer"},"type":"array","title":"Roms","description":"List of rom ids to delete from database."},"delete_from_fs":{"items":{"type":"integer"},"type":"array","title":"Delete From Fs","description":"List of rom ids to delete from filesystem."}},"type":"object","required":["roms"],"title":"Body_delete_roms_api_roms_delete_post"},"Body_delete_saves_api_saves_delete_post":{"properties":{"saves":{"items":{"type":"integer"},"type":"array","title":"Saves","description":"List of save ids to delete from database."}},"type":"object","required":["saves"],"title":"Body_delete_saves_api_saves_delete_post"},"Body_delete_states_api_states_delete_post":{"properties":{"states":{"items":{"type":"integer"},"type":"array","title":"States","description":"List of states ids to delete from database."}},"type":"object","required":["states"],"title":"Body_delete_states_api_states_delete_post"},"Body_refresh_retro_achievements_api_users__id__ra_refresh_post":{"properties":{"incremental":{"type":"boolean","title":"Incremental","description":"Whether to only retrieve RetroAchievements progression incrementally.","default":false}},"type":"object","title":"Body_refresh_retro_achievements_api_users__id__ra_refresh_post"},"Body_request_password_reset_api_forgot_password_post":{"properties":{"username":{"type":"string","title":"Username"}},"type":"object","required":["username"],"title":"Body_request_password_reset_api_forgot_password_post"},"Body_reset_password_api_reset_password_post":{"properties":{"token":{"type":"string","title":"Token"},"new_password":{"type":"string","title":"New Password"}},"type":"object","required":["token","new_password"],"title":"Body_reset_password_api_reset_password_post"},"Body_token_api_token_post":{"properties":{"grant_type":{"type":"string","title":"Grant Type","default":"password"},"scope":{"type":"string","title":"Scope","default":""},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Secret"},"refresh_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Token"}},"type":"object","title":"Body_token_api_token_post"},"Body_update_collection_api_collections__id__put":{"properties":{"artwork":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Artwork"}},"type":"object","title":"Body_update_collection_api_collections__id__put"},"Body_update_platform_api_platforms__id__put":{"properties":{"aspect_ratio":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aspect Ratio","description":"Cover aspect ratio."},"custom_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Name","description":"Custom platform name."}},"type":"object","title":"Body_update_platform_api_platforms__id__put"},"Body_update_rom_api_roms__id__put":{"properties":{"artwork":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Artwork","description":"Custom artwork to set as cover."}},"type":"object","title":"Body_update_rom_api_roms__id__put"},"Body_update_rom_user_api_roms__id__props_put":{"properties":{"update_last_played":{"type":"boolean","title":"Update Last Played","description":"Whether to update the last played date.","default":false},"remove_last_played":{"type":"boolean","title":"Remove Last Played","description":"Whether to remove the last played date.","default":false}},"type":"object","title":"Body_update_rom_user_api_roms__id__props_put"},"BulkOperationResponse":{"properties":{"successful_items":{"type":"integer","title":"Successful Items"},"failed_items":{"type":"integer","title":"Failed Items"},"errors":{"items":{"type":"string"},"type":"array","title":"Errors"}},"type":"object","required":["successful_items","failed_items","errors"],"title":"BulkOperationResponse"},"CleanupStats":{"properties":{"platforms_in_db":{"type":"integer","title":"Platforms In Db"},"roms_in_db":{"type":"integer","title":"Roms In Db"},"platforms_in_fs":{"type":"integer","title":"Platforms In Fs"},"roms_in_fs":{"type":"integer","title":"Roms In Fs"},"removed_fs_platforms":{"type":"integer","title":"Removed Fs Platforms"},"removed_fs_roms":{"type":"integer","title":"Removed Fs Roms"}},"type":"object","required":["platforms_in_db","roms_in_db","platforms_in_fs","roms_in_fs","removed_fs_platforms","removed_fs_roms"],"title":"CleanupStats"},"CleanupTaskMeta":{"properties":{"cleanup_stats":{"anyOf":[{"$ref":"#/components/schemas/CleanupStats"},{"type":"null"}]}},"type":"object","required":["cleanup_stats"],"title":"CleanupTaskMeta"},"CleanupTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"cleanup","title":"Task Type"},"meta":{"$ref":"#/components/schemas/CleanupTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"CleanupTaskStatusResponse"},"CollectionSchema":{"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"rom_ids":{"items":{"type":"integer"},"type":"array","uniqueItems":true,"title":"Rom Ids"},"rom_count":{"type":"integer","title":"Rom Count"},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"path_covers_small":{"items":{"type":"string"},"type":"array","title":"Path Covers Small"},"path_covers_large":{"items":{"type":"string"},"type":"array","title":"Path Covers Large"},"is_public":{"type":"boolean","title":"Is Public","default":false},"is_favorite":{"type":"boolean","title":"Is Favorite","default":false},"is_virtual":{"type":"boolean","title":"Is Virtual","default":false},"is_smart":{"type":"boolean","title":"Is Smart","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"id":{"type":"integer","title":"Id"},"url_cover":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Cover"},"user_id":{"type":"integer","title":"User Id"},"user__username":{"type":"string","title":"User Username"}},"type":"object","required":["name","description","rom_ids","rom_count","path_cover_small","path_cover_large","path_covers_small","path_covers_large","created_at","updated_at","id","url_cover","user_id","user__username"],"title":"CollectionSchema"},"ConfigResponse":{"properties":{"CONFIG_FILE_MOUNTED":{"type":"boolean","title":"Config File Mounted"},"CONFIG_FILE_WRITABLE":{"type":"boolean","title":"Config File Writable"},"EXCLUDED_PLATFORMS":{"items":{"type":"string"},"type":"array","title":"Excluded Platforms"},"EXCLUDED_SINGLE_EXT":{"items":{"type":"string"},"type":"array","title":"Excluded Single Ext"},"EXCLUDED_SINGLE_FILES":{"items":{"type":"string"},"type":"array","title":"Excluded Single Files"},"EXCLUDED_MULTI_FILES":{"items":{"type":"string"},"type":"array","title":"Excluded Multi Files"},"EXCLUDED_MULTI_PARTS_EXT":{"items":{"type":"string"},"type":"array","title":"Excluded Multi Parts Ext"},"EXCLUDED_MULTI_PARTS_FILES":{"items":{"type":"string"},"type":"array","title":"Excluded Multi Parts Files"},"PLATFORMS_BINDING":{"additionalProperties":{"type":"string"},"type":"object","title":"Platforms Binding"},"PLATFORMS_VERSIONS":{"additionalProperties":{"type":"string"},"type":"object","title":"Platforms Versions"},"SKIP_HASH_CALCULATION":{"type":"boolean","title":"Skip Hash Calculation"},"EJS_DEBUG":{"type":"boolean","title":"Ejs Debug"},"EJS_CACHE_LIMIT":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ejs Cache Limit"},"EJS_DISABLE_AUTO_UNLOAD":{"type":"boolean","title":"Ejs Disable Auto Unload"},"EJS_DISABLE_BATCH_BOOTUP":{"type":"boolean","title":"Ejs Disable Batch Bootup"},"EJS_NETPLAY_ENABLED":{"type":"boolean","title":"Ejs Netplay Enabled"},"EJS_NETPLAY_ICE_SERVERS":{"items":{"$ref":"#/components/schemas/NetplayICEServer"},"type":"array","title":"Ejs Netplay Ice Servers"},"EJS_SETTINGS":{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object","title":"Ejs Settings"},"EJS_CONTROLS":{"additionalProperties":{"$ref":"#/components/schemas/EjsControls"},"type":"object","title":"Ejs Controls"},"SCAN_METADATA_PRIORITY":{"items":{"type":"string"},"type":"array","title":"Scan Metadata Priority"},"SCAN_ARTWORK_PRIORITY":{"items":{"type":"string"},"type":"array","title":"Scan Artwork Priority"},"SCAN_REGION_PRIORITY":{"items":{"type":"string"},"type":"array","title":"Scan Region Priority"},"SCAN_LANGUAGE_PRIORITY":{"items":{"type":"string"},"type":"array","title":"Scan Language Priority"},"SCAN_MEDIA":{"items":{"type":"string"},"type":"array","title":"Scan Media"}},"type":"object","required":["CONFIG_FILE_MOUNTED","CONFIG_FILE_WRITABLE","EXCLUDED_PLATFORMS","EXCLUDED_SINGLE_EXT","EXCLUDED_SINGLE_FILES","EXCLUDED_MULTI_FILES","EXCLUDED_MULTI_PARTS_EXT","EXCLUDED_MULTI_PARTS_FILES","PLATFORMS_BINDING","PLATFORMS_VERSIONS","SKIP_HASH_CALCULATION","EJS_DEBUG","EJS_CACHE_LIMIT","EJS_DISABLE_AUTO_UNLOAD","EJS_DISABLE_BATCH_BOOTUP","EJS_NETPLAY_ENABLED","EJS_NETPLAY_ICE_SERVERS","EJS_SETTINGS","EJS_CONTROLS","SCAN_METADATA_PRIORITY","SCAN_ARTWORK_PRIORITY","SCAN_REGION_PRIORITY","SCAN_LANGUAGE_PRIORITY","SCAN_MEDIA"],"title":"ConfigResponse"},"ConversionStats":{"properties":{"processed":{"type":"integer","title":"Processed"},"errors":{"type":"integer","title":"Errors"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["processed","errors","total"],"title":"ConversionStats"},"ConversionTaskMeta":{"properties":{"conversion_stats":{"anyOf":[{"$ref":"#/components/schemas/ConversionStats"},{"type":"null"}]}},"type":"object","required":["conversion_stats"],"title":"ConversionTaskMeta"},"ConversionTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"conversion","title":"Task Type"},"meta":{"$ref":"#/components/schemas/ConversionTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"ConversionTaskStatusResponse"},"CustomLimitOffsetPage_SimpleRomSchema_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SimpleRomSchema"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"limit":{"type":"integer","minimum":1.0,"title":"Limit"},"offset":{"type":"integer","minimum":0.0,"title":"Offset"},"char_index":{"additionalProperties":{"type":"integer"},"type":"object","title":"Char Index"},"rom_id_index":{"items":{"type":"integer"},"type":"array","title":"Rom Id Index"},"filter_values":{"$ref":"#/components/schemas/RomFiltersDict"}},"type":"object","required":["items","total","limit","offset","char_index","rom_id_index","filter_values"],"title":"CustomLimitOffsetPage[SimpleRomSchema]"},"DetailedRomSchema":{"properties":{"id":{"type":"integer","title":"Id"},"igdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Igdb Id"},"sgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sgdb Id"},"moby_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Moby Id"},"ss_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ss Id"},"ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ra Id"},"launchbox_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Launchbox Id"},"hasheous_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hasheous Id"},"tgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tgdb Id"},"flashpoint_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flashpoint Id"},"hltb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hltb Id"},"gamelist_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gamelist Id"},"platform_id":{"type":"integer","title":"Platform Id"},"platform_slug":{"type":"string","title":"Platform Slug"},"platform_fs_slug":{"type":"string","title":"Platform Fs Slug"},"platform_custom_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform Custom Name"},"platform_display_name":{"type":"string","title":"Platform Display Name"},"fs_name":{"type":"string","title":"Fs Name"},"fs_name_no_tags":{"type":"string","title":"Fs Name No Tags"},"fs_name_no_ext":{"type":"string","title":"Fs Name No Ext"},"fs_extension":{"type":"string","title":"Fs Extension"},"fs_path":{"type":"string","title":"Fs Path"},"fs_size_bytes":{"type":"integer","title":"Fs Size Bytes"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"alternative_names":{"items":{"type":"string"},"type":"array","title":"Alternative Names"},"youtube_video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Video Id"},"metadatum":{"$ref":"#/components/schemas/RomMetadataSchema"},"igdb_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomIGDBMetadata"},{"type":"null"}]},"moby_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomMobyMetadata"},{"type":"null"}]},"ss_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomSSMetadata"},{"type":"null"}]},"launchbox_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomLaunchboxMetadata"},{"type":"null"}]},"hasheous_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomHasheousMetadata"},{"type":"null"}]},"flashpoint_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomFlashpointMetadata"},{"type":"null"}]},"hltb_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomHLTBMetadata"},{"type":"null"}]},"gamelist_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomGamelistMetadata"},{"type":"null"}]},"manual_metadata":{"anyOf":[{"$ref":"#/components/schemas/ManualMetadata"},{"type":"null"}]},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"url_cover":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Cover"},"has_manual":{"type":"boolean","title":"Has Manual"},"path_manual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Manual"},"url_manual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Manual"},"is_identifying":{"type":"boolean","title":"Is Identifying","default":false},"is_unidentified":{"type":"boolean","title":"Is Unidentified"},"is_identified":{"type":"boolean","title":"Is Identified"},"revision":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revision"},"regions":{"items":{"type":"string"},"type":"array","title":"Regions"},"languages":{"items":{"type":"string"},"type":"array","title":"Languages"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"crc_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crc Hash"},"md5_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5 Hash"},"sha1_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sha1 Hash"},"has_simple_single_file":{"type":"boolean","title":"Has Simple Single File"},"has_nested_single_file":{"type":"boolean","title":"Has Nested Single File"},"has_multiple_files":{"type":"boolean","title":"Has Multiple Files"},"files":{"items":{"$ref":"#/components/schemas/RomFileSchema"},"type":"array","title":"Files"},"full_path":{"type":"string","title":"Full Path"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"has_notes":{"type":"boolean","title":"Has Notes"},"siblings":{"items":{"$ref":"#/components/schemas/SiblingRomSchema"},"type":"array","title":"Siblings"},"rom_user":{"$ref":"#/components/schemas/RomUserSchema"},"merged_screenshots":{"items":{"type":"string"},"type":"array","title":"Merged Screenshots"},"merged_ra_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomRAMetadata"},{"type":"null"}]},"user_saves":{"items":{"$ref":"#/components/schemas/SaveSchema"},"type":"array","title":"User Saves"},"user_states":{"items":{"$ref":"#/components/schemas/StateSchema"},"type":"array","title":"User States"},"user_screenshots":{"items":{"$ref":"#/components/schemas/ScreenshotSchema"},"type":"array","title":"User Screenshots"},"user_collections":{"items":{"$ref":"#/components/schemas/UserCollectionSchema"},"type":"array","title":"User Collections"},"all_user_notes":{"items":{"$ref":"#/components/schemas/UserNoteSchema"},"type":"array","title":"All User Notes"}},"type":"object","required":["id","igdb_id","sgdb_id","moby_id","ss_id","ra_id","launchbox_id","hasheous_id","tgdb_id","flashpoint_id","hltb_id","gamelist_id","platform_id","platform_slug","platform_fs_slug","platform_custom_name","platform_display_name","fs_name","fs_name_no_tags","fs_name_no_ext","fs_extension","fs_path","fs_size_bytes","name","slug","summary","alternative_names","youtube_video_id","metadatum","igdb_metadata","moby_metadata","ss_metadata","launchbox_metadata","hasheous_metadata","flashpoint_metadata","hltb_metadata","gamelist_metadata","manual_metadata","path_cover_small","path_cover_large","url_cover","has_manual","path_manual","url_manual","is_unidentified","is_identified","revision","regions","languages","tags","crc_hash","md5_hash","sha1_hash","has_simple_single_file","has_nested_single_file","has_multiple_files","files","full_path","created_at","updated_at","missing_from_fs","has_notes","siblings","rom_user","merged_screenshots","merged_ra_metadata","user_saves","user_states","user_screenshots","user_collections","all_user_notes"],"title":"DetailedRomSchema"},"EarnedAchievement":{"properties":{"id":{"type":"string","title":"Id"},"date":{"type":"string","title":"Date"},"date_hardcore":{"type":"string","title":"Date Hardcore"}},"type":"object","required":["id","date"],"title":"EarnedAchievement"},"EjsControls":{"properties":{"_0":{"additionalProperties":{"$ref":"#/components/schemas/EjsControlsButton"},"type":"object","title":"0"},"_1":{"additionalProperties":{"$ref":"#/components/schemas/EjsControlsButton"},"type":"object","title":"1"},"_2":{"additionalProperties":{"$ref":"#/components/schemas/EjsControlsButton"},"type":"object","title":"2"},"_3":{"additionalProperties":{"$ref":"#/components/schemas/EjsControlsButton"},"type":"object","title":"3"}},"type":"object","required":["_0","_1","_2","_3"],"title":"EjsControls"},"EjsControlsButton":{"properties":{"value":{"type":"string","title":"Value"},"value2":{"type":"string","title":"Value2"}},"type":"object","title":"EjsControlsButton"},"EmulationDict":{"properties":{"DISABLE_EMULATOR_JS":{"type":"boolean","title":"Disable Emulator Js"},"DISABLE_RUFFLE_RS":{"type":"boolean","title":"Disable Ruffle Rs"}},"type":"object","required":["DISABLE_EMULATOR_JS","DISABLE_RUFFLE_RS"],"title":"EmulationDict"},"FilesystemDict":{"properties":{"FS_PLATFORMS":{"items":{"type":"string"},"type":"array","title":"Fs Platforms"}},"type":"object","required":["FS_PLATFORMS"],"title":"FilesystemDict"},"FirmwareSchema":{"properties":{"id":{"type":"integer","title":"Id"},"file_name":{"type":"string","title":"File Name"},"file_name_no_tags":{"type":"string","title":"File Name No Tags"},"file_name_no_ext":{"type":"string","title":"File Name No Ext"},"file_extension":{"type":"string","title":"File Extension"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"is_verified":{"type":"boolean","title":"Is Verified"},"crc_hash":{"type":"string","title":"Crc Hash"},"md5_hash":{"type":"string","title":"Md5 Hash"},"sha1_hash":{"type":"string","title":"Sha1 Hash"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","file_name","file_name_no_tags","file_name_no_ext","file_extension","file_path","file_size_bytes","full_path","is_verified","crc_hash","md5_hash","sha1_hash","missing_from_fs","created_at","updated_at"],"title":"FirmwareSchema"},"FrontendDict":{"properties":{"UPLOAD_TIMEOUT":{"type":"integer","title":"Upload Timeout"},"DISABLE_USERPASS_LOGIN":{"type":"boolean","title":"Disable Userpass Login"},"YOUTUBE_BASE_URL":{"type":"string","title":"Youtube Base Url"}},"type":"object","required":["UPLOAD_TIMEOUT","DISABLE_USERPASS_LOGIN","YOUTUBE_BASE_URL"],"title":"FrontendDict"},"GenericTaskMeta":{"properties":{},"type":"object","title":"GenericTaskMeta"},"GenericTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"generic","title":"Task Type"},"meta":{"$ref":"#/components/schemas/GenericTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"GenericTaskStatusResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HeartbeatResponse":{"properties":{"SYSTEM":{"$ref":"#/components/schemas/SystemDict"},"METADATA_SOURCES":{"$ref":"#/components/schemas/MetadataSourcesDict"},"FILESYSTEM":{"$ref":"#/components/schemas/FilesystemDict"},"EMULATION":{"$ref":"#/components/schemas/EmulationDict"},"FRONTEND":{"$ref":"#/components/schemas/FrontendDict"},"OIDC":{"$ref":"#/components/schemas/OIDCDict"},"TASKS":{"$ref":"#/components/schemas/TasksDict"}},"type":"object","required":["SYSTEM","METADATA_SOURCES","FILESYSTEM","EMULATION","FRONTEND","OIDC","TASKS"],"title":"HeartbeatResponse"},"IGDBAgeRating":{"properties":{"rating":{"type":"string","title":"Rating"},"category":{"type":"string","title":"Category"},"rating_cover_url":{"type":"string","title":"Rating Cover Url"}},"type":"object","required":["rating","category","rating_cover_url"],"title":"IGDBAgeRating"},"IGDBMetadataMultiplayerMode":{"properties":{"campaigncoop":{"type":"boolean","title":"Campaigncoop"},"dropin":{"type":"boolean","title":"Dropin"},"lancoop":{"type":"boolean","title":"Lancoop"},"offlinecoop":{"type":"boolean","title":"Offlinecoop"},"offlinecoopmax":{"type":"integer","title":"Offlinecoopmax"},"offlinemax":{"type":"integer","title":"Offlinemax"},"onlinecoop":{"type":"integer","title":"Onlinecoop"},"onlinecoopmax":{"type":"integer","title":"Onlinecoopmax"},"onlinemax":{"type":"integer","title":"Onlinemax"},"splitscreen":{"type":"boolean","title":"Splitscreen"},"splitscreenonline":{"type":"boolean","title":"Splitscreenonline"},"platform":{"$ref":"#/components/schemas/IGDBMetadataPlatform"}},"type":"object","required":["campaigncoop","dropin","lancoop","offlinecoop","offlinecoopmax","offlinemax","onlinecoop","onlinecoopmax","onlinemax","splitscreen","splitscreenonline","platform"],"title":"IGDBMetadataMultiplayerMode"},"IGDBMetadataPlatform":{"properties":{"igdb_id":{"type":"integer","title":"Igdb Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["igdb_id","name"],"title":"IGDBMetadataPlatform"},"IGDBRelatedGame":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"type":{"type":"string","title":"Type"},"cover_url":{"type":"string","title":"Cover Url"}},"type":"object","required":["id","name","slug","type","cover_url"],"title":"IGDBRelatedGame"},"InviteLinkSchema":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"InviteLinkSchema"},"JobStatus":{"type":"string","enum":["queued","finished","failed","started","deferred","scheduled","stopped","canceled"],"title":"JobStatus","description":"The Status of Job within its lifecycle at any given time."},"LaunchboxImage":{"properties":{"url":{"type":"string","title":"Url"},"type":{"type":"string","title":"Type"},"region":{"type":"string","title":"Region"}},"type":"object","required":["url"],"title":"LaunchboxImage"},"ManualMetadata":{"properties":{"genres":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Genres"},"franchises":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Franchises"},"companies":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Companies"},"game_modes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Game Modes"},"age_ratings":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Age Ratings"},"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"youtube_video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Video Id"}},"type":"object","title":"ManualMetadata"},"MetadataSourcesDict":{"properties":{"ANY_SOURCE_ENABLED":{"type":"boolean","title":"Any Source Enabled"},"IGDB_API_ENABLED":{"type":"boolean","title":"Igdb Api Enabled"},"SS_API_ENABLED":{"type":"boolean","title":"Ss Api Enabled"},"MOBY_API_ENABLED":{"type":"boolean","title":"Moby Api Enabled"},"STEAMGRIDDB_API_ENABLED":{"type":"boolean","title":"Steamgriddb Api Enabled"},"RA_API_ENABLED":{"type":"boolean","title":"Ra Api Enabled"},"LAUNCHBOX_API_ENABLED":{"type":"boolean","title":"Launchbox Api Enabled"},"HASHEOUS_API_ENABLED":{"type":"boolean","title":"Hasheous Api Enabled"},"PLAYMATCH_API_ENABLED":{"type":"boolean","title":"Playmatch Api Enabled"},"TGDB_API_ENABLED":{"type":"boolean","title":"Tgdb Api Enabled"},"FLASHPOINT_API_ENABLED":{"type":"boolean","title":"Flashpoint Api Enabled"},"HLTB_API_ENABLED":{"type":"boolean","title":"Hltb Api Enabled"}},"type":"object","required":["ANY_SOURCE_ENABLED","IGDB_API_ENABLED","SS_API_ENABLED","MOBY_API_ENABLED","STEAMGRIDDB_API_ENABLED","RA_API_ENABLED","LAUNCHBOX_API_ENABLED","HASHEOUS_API_ENABLED","PLAYMATCH_API_ENABLED","TGDB_API_ENABLED","FLASHPOINT_API_ENABLED","HLTB_API_ENABLED"],"title":"MetadataSourcesDict"},"MobyMetadataPlatform":{"properties":{"moby_id":{"type":"integer","title":"Moby Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["moby_id","name"],"title":"MobyMetadataPlatform"},"NetplayICEServer":{"properties":{"urls":{"type":"string","title":"Urls"},"username":{"type":"string","title":"Username"},"credential":{"type":"string","title":"Credential"}},"type":"object","required":["urls"],"title":"NetplayICEServer"},"OIDCDict":{"properties":{"ENABLED":{"type":"boolean","title":"Enabled"},"PROVIDER":{"type":"string","title":"Provider"}},"type":"object","required":["ENABLED","PROVIDER"],"title":"OIDCDict"},"PlatformSchema":{"properties":{"id":{"type":"integer","title":"Id"},"slug":{"type":"string","title":"Slug"},"fs_slug":{"type":"string","title":"Fs Slug"},"rom_count":{"type":"integer","title":"Rom Count"},"name":{"type":"string","title":"Name"},"igdb_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Igdb Slug"},"moby_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Moby Slug"},"hltb_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hltb Slug"},"custom_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Name"},"igdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Igdb Id"},"sgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sgdb Id"},"moby_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Moby Id"},"launchbox_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Launchbox Id"},"ss_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ss Id"},"ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ra Id"},"hasheous_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hasheous Id"},"tgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tgdb Id"},"flashpoint_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Flashpoint Id"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"generation":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Generation"},"family_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Family Name"},"family_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Family Slug"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"url_logo":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Logo"},"firmware":{"items":{"$ref":"#/components/schemas/FirmwareSchema"},"type":"array","title":"Firmware"},"aspect_ratio":{"type":"string","title":"Aspect Ratio","default":"2 / 3"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"fs_size_bytes":{"type":"integer","title":"Fs Size Bytes"},"is_unidentified":{"type":"boolean","title":"Is Unidentified"},"is_identified":{"type":"boolean","title":"Is Identified"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"display_name":{"type":"string","title":"Display Name","readOnly":true}},"type":"object","required":["id","slug","fs_slug","rom_count","name","igdb_slug","moby_slug","hltb_slug","created_at","updated_at","fs_size_bytes","is_unidentified","is_identified","missing_from_fs","display_name"],"title":"PlatformSchema"},"RAGameRomAchievement":{"properties":{"ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ra Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"points":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Points"},"num_awarded":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Num Awarded"},"num_awarded_hardcore":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Num Awarded Hardcore"},"badge_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Id"},"badge_url_lock":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Url Lock"},"badge_path_lock":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Path Lock"},"badge_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Url"},"badge_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Path"},"display_order":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Display Order"},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type"}},"type":"object","required":["ra_id","title","description","points","num_awarded","num_awarded_hardcore","badge_id","badge_url_lock","badge_path_lock","badge_url","badge_path","display_order","type"],"title":"RAGameRomAchievement"},"RAProgression":{"properties":{"total":{"type":"integer","title":"Total"},"results":{"items":{"$ref":"#/components/schemas/RAUserGameProgression"},"type":"array","title":"Results"}},"type":"object","title":"RAProgression"},"RAUserGameProgression":{"properties":{"rom_ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rom Ra Id"},"max_possible":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Possible"},"num_awarded":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Num Awarded"},"num_awarded_hardcore":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Num Awarded Hardcore"},"most_recent_awarded_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Most Recent Awarded Date"},"earned_achievements":{"items":{"$ref":"#/components/schemas/EarnedAchievement"},"type":"array","title":"Earned Achievements"}},"type":"object","required":["rom_ra_id","max_possible","num_awarded","num_awarded_hardcore","earned_achievements"],"title":"RAUserGameProgression"},"Role":{"type":"string","enum":["viewer","editor","admin"],"title":"Role"},"RomFileCategory":{"type":"string","enum":["game","dlc","hack","manual","patch","update","mod","demo","translation","prototype","cheat"],"title":"RomFileCategory"},"RomFileSchema":{"properties":{"id":{"type":"integer","title":"Id"},"rom_id":{"type":"integer","title":"Rom Id"},"file_name":{"type":"string","title":"File Name"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"last_modified":{"type":"string","format":"date-time","title":"Last Modified"},"crc_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crc Hash"},"md5_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5 Hash"},"sha1_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sha1 Hash"},"category":{"anyOf":[{"$ref":"#/components/schemas/RomFileCategory"},{"type":"null"}]}},"type":"object","required":["id","rom_id","file_name","file_path","file_size_bytes","full_path","created_at","updated_at","last_modified","crc_hash","md5_hash","sha1_hash","category"],"title":"RomFileSchema"},"RomFiltersDict":{"properties":{"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"collections":{"items":{"type":"string"},"type":"array","title":"Collections"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"age_ratings":{"items":{"type":"string"},"type":"array","title":"Age Ratings"},"player_counts":{"items":{"type":"string"},"type":"array","title":"Player Counts"},"regions":{"items":{"type":"string"},"type":"array","title":"Regions"},"languages":{"items":{"type":"string"},"type":"array","title":"Languages"},"platforms":{"items":{"type":"integer"},"type":"array","title":"Platforms"}},"type":"object","required":["genres","franchises","collections","companies","game_modes","age_ratings","player_counts","regions","languages","platforms"],"title":"RomFiltersDict"},"RomFlashpointMetadata":{"properties":{"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"first_release_date":{"type":"string","title":"First Release Date"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"RomFlashpointMetadata"},"RomGamelistMetadata":{"properties":{"box2d_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Url"},"box2d_back_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Back Url"},"box3d_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box3D Url"},"fanart_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fanart Url"},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url"},"manual_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manual Url"},"marquee_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marquee Url"},"miximage_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Miximage Url"},"physical_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical Url"},"screenshot_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screenshot Url"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url"},"title_screen_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title Screen Url"},"video_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Url"},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating"},"first_release_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Release Date"},"companies":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Companies"},"franchises":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Franchises"},"genres":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Genres"},"player_count":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Player Count"},"md5_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5 Hash"},"box3d_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box3D Path"},"miximage_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Miximage Path"},"physical_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical Path"},"marquee_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marquee Path"},"video_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Path"}},"type":"object","title":"RomGamelistMetadata"},"RomHLTBMetadata":{"properties":{"main_story":{"type":"integer","title":"Main Story"},"main_story_count":{"type":"integer","title":"Main Story Count"},"main_plus_extra":{"type":"integer","title":"Main Plus Extra"},"main_plus_extra_count":{"type":"integer","title":"Main Plus Extra Count"},"completionist":{"type":"integer","title":"Completionist"},"completionist_count":{"type":"integer","title":"Completionist Count"},"all_styles":{"type":"integer","title":"All Styles"},"all_styles_count":{"type":"integer","title":"All Styles Count"},"release_year":{"type":"integer","title":"Release Year"},"review_score":{"type":"integer","title":"Review Score"},"review_count":{"type":"integer","title":"Review Count"},"popularity":{"type":"integer","title":"Popularity"},"completions":{"type":"integer","title":"Completions"}},"type":"object","title":"RomHLTBMetadata"},"RomHasheousMetadata":{"properties":{"tosec_match":{"type":"boolean","title":"Tosec Match"},"mame_arcade_match":{"type":"boolean","title":"Mame Arcade Match"},"mame_mess_match":{"type":"boolean","title":"Mame Mess Match"},"nointro_match":{"type":"boolean","title":"Nointro Match"},"redump_match":{"type":"boolean","title":"Redump Match"},"whdload_match":{"type":"boolean","title":"Whdload Match"},"ra_match":{"type":"boolean","title":"Ra Match"},"fbneo_match":{"type":"boolean","title":"Fbneo Match"},"puredos_match":{"type":"boolean","title":"Puredos Match"}},"type":"object","title":"RomHasheousMetadata"},"RomIGDBMetadata":{"properties":{"total_rating":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Total Rating"},"aggregated_rating":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aggregated Rating"},"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"youtube_video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Video Id"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"alternative_names":{"items":{"type":"string"},"type":"array","title":"Alternative Names"},"collections":{"items":{"type":"string"},"type":"array","title":"Collections"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"age_ratings":{"items":{"$ref":"#/components/schemas/IGDBAgeRating"},"type":"array","title":"Age Ratings"},"platforms":{"items":{"$ref":"#/components/schemas/IGDBMetadataPlatform"},"type":"array","title":"Platforms"},"multiplayer_modes":{"items":{"$ref":"#/components/schemas/IGDBMetadataMultiplayerMode"},"type":"array","title":"Multiplayer Modes"},"player_count":{"type":"string","title":"Player Count"},"expansions":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Expansions"},"dlcs":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Dlcs"},"remasters":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Remasters"},"remakes":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Remakes"},"expanded_games":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Expanded Games"},"ports":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Ports"},"similar_games":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Similar Games"}},"type":"object","title":"RomIGDBMetadata"},"RomLaunchboxMetadata":{"properties":{"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"max_players":{"type":"integer","title":"Max Players"},"release_type":{"type":"string","title":"Release Type"},"cooperative":{"type":"boolean","title":"Cooperative"},"youtube_video_id":{"type":"string","title":"Youtube Video Id"},"community_rating":{"type":"number","title":"Community Rating"},"community_rating_count":{"type":"integer","title":"Community Rating Count"},"wikipedia_url":{"type":"string","title":"Wikipedia Url"},"esrb":{"type":"string","title":"Esrb"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"images":{"items":{"$ref":"#/components/schemas/LaunchboxImage"},"type":"array","title":"Images"}},"type":"object","title":"RomLaunchboxMetadata"},"RomMetadataSchema":{"properties":{"rom_id":{"type":"integer","title":"Rom Id"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"collections":{"items":{"type":"string"},"type":"array","title":"Collections"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"age_ratings":{"items":{"type":"string"},"type":"array","title":"Age Ratings"},"player_count":{"type":"string","title":"Player Count"},"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"average_rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Average Rating"}},"type":"object","required":["rom_id","genres","franchises","collections","companies","game_modes","age_ratings","player_count","first_release_date","average_rating"],"title":"RomMetadataSchema"},"RomMobyMetadata":{"properties":{"moby_score":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Moby Score"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"alternate_titles":{"items":{"type":"string"},"type":"array","title":"Alternate Titles"},"platforms":{"items":{"$ref":"#/components/schemas/MobyMetadataPlatform"},"type":"array","title":"Platforms"}},"type":"object","title":"RomMobyMetadata"},"RomRAMetadata":{"properties":{"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"achievements":{"items":{"$ref":"#/components/schemas/RAGameRomAchievement"},"type":"array","title":"Achievements"}},"type":"object","title":"RomRAMetadata"},"RomSSMetadata":{"properties":{"bezel_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bezel Url"},"box2d_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Url"},"box2d_side_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Side Url"},"box2d_back_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Back Url"},"box3d_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box3D Url"},"fanart_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fanart Url"},"fullbox_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fullbox Url"},"logo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Url"},"manual_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manual Url"},"marquee_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marquee Url"},"miximage_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Miximage Url"},"physical_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical Url"},"screenshot_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screenshot Url"},"steamgrid_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Steamgrid Url"},"title_screen_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title Screen Url"},"video_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Url"},"video_normalized_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Normalized Url"},"bezel_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bezel Path"},"box2d_back_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Back Path"},"box3d_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box3D Path"},"fanart_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fanart Path"},"miximage_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Miximage Path"},"physical_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical Path"},"marquee_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marquee Path"},"logo_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Path"},"video_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Path"},"ss_score":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ss Score"},"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"alternative_names":{"items":{"type":"string"},"type":"array","title":"Alternative Names"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"player_count":{"type":"string","title":"Player Count"}},"type":"object","title":"RomSSMetadata"},"RomUserSchema":{"properties":{"id":{"type":"integer","title":"Id"},"user_id":{"type":"integer","title":"User Id"},"rom_id":{"type":"integer","title":"Rom Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"last_played":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Played"},"is_main_sibling":{"type":"boolean","title":"Is Main Sibling"},"backlogged":{"type":"boolean","title":"Backlogged"},"now_playing":{"type":"boolean","title":"Now Playing"},"hidden":{"type":"boolean","title":"Hidden"},"rating":{"type":"integer","title":"Rating"},"difficulty":{"type":"integer","title":"Difficulty"},"completion":{"type":"integer","title":"Completion"},"status":{"anyOf":[{"$ref":"#/components/schemas/RomUserStatus"},{"type":"null"}]},"user__username":{"type":"string","title":"User Username"}},"type":"object","required":["id","user_id","rom_id","created_at","updated_at","last_played","is_main_sibling","backlogged","now_playing","hidden","rating","difficulty","completion","status","user__username"],"title":"RomUserSchema"},"RomUserStatus":{"type":"string","enum":["incomplete","finished","completed_100","retired","never_playing"],"title":"RomUserStatus"},"RoomsResponse":{"properties":{"room_name":{"type":"string","title":"Room Name"},"current":{"type":"integer","title":"Current"},"max":{"type":"integer","title":"Max"},"player_name":{"type":"string","title":"Player Name"},"hasPassword":{"type":"boolean","title":"Haspassword"}},"type":"object","required":["room_name","current","max","player_name","hasPassword"],"title":"RoomsResponse"},"SGDBResource":{"properties":{"thumb":{"type":"string","title":"Thumb"},"url":{"type":"string","title":"Url"},"type":{"type":"string","title":"Type"}},"type":"object","required":["thumb","url","type"],"title":"SGDBResource"},"SaveSchema":{"properties":{"id":{"type":"integer","title":"Id"},"rom_id":{"type":"integer","title":"Rom Id"},"user_id":{"type":"integer","title":"User Id"},"file_name":{"type":"string","title":"File Name"},"file_name_no_tags":{"type":"string","title":"File Name No Tags"},"file_name_no_ext":{"type":"string","title":"File Name No Ext"},"file_extension":{"type":"string","title":"File Extension"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"download_path":{"type":"string","title":"Download Path"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"emulator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emulator"},"screenshot":{"anyOf":[{"$ref":"#/components/schemas/ScreenshotSchema"},{"type":"null"}]}},"type":"object","required":["id","rom_id","user_id","file_name","file_name_no_tags","file_name_no_ext","file_extension","file_path","file_size_bytes","full_path","download_path","missing_from_fs","created_at","updated_at","emulator","screenshot"],"title":"SaveSchema"},"ScanStats":{"properties":{"total_platforms":{"type":"integer","title":"Total Platforms"},"total_roms":{"type":"integer","title":"Total Roms"},"scanned_platforms":{"type":"integer","title":"Scanned Platforms"},"new_platforms":{"type":"integer","title":"New Platforms"},"identified_platforms":{"type":"integer","title":"Identified Platforms"},"scanned_roms":{"type":"integer","title":"Scanned Roms"},"new_roms":{"type":"integer","title":"New Roms"},"identified_roms":{"type":"integer","title":"Identified Roms"},"scanned_firmware":{"type":"integer","title":"Scanned Firmware"},"new_firmware":{"type":"integer","title":"New Firmware"}},"type":"object","required":["total_platforms","total_roms","scanned_platforms","new_platforms","identified_platforms","scanned_roms","new_roms","identified_roms","scanned_firmware","new_firmware"],"title":"ScanStats"},"ScanTaskMeta":{"properties":{"scan_stats":{"anyOf":[{"$ref":"#/components/schemas/ScanStats"},{"type":"null"}]}},"type":"object","required":["scan_stats"],"title":"ScanTaskMeta"},"ScanTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"scan","title":"Task Type"},"meta":{"$ref":"#/components/schemas/ScanTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"ScanTaskStatusResponse"},"ScreenshotSchema":{"properties":{"id":{"type":"integer","title":"Id"},"rom_id":{"type":"integer","title":"Rom Id"},"user_id":{"type":"integer","title":"User Id"},"file_name":{"type":"string","title":"File Name"},"file_name_no_tags":{"type":"string","title":"File Name No Tags"},"file_name_no_ext":{"type":"string","title":"File Name No Ext"},"file_extension":{"type":"string","title":"File Extension"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"download_path":{"type":"string","title":"Download Path"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","rom_id","user_id","file_name","file_name_no_tags","file_name_no_ext","file_extension","file_path","file_size_bytes","full_path","download_path","missing_from_fs","created_at","updated_at"],"title":"ScreenshotSchema"},"SearchCoverSchema":{"properties":{"name":{"type":"string","title":"Name"},"resources":{"items":{"$ref":"#/components/schemas/SGDBResource"},"type":"array","title":"Resources"}},"type":"object","required":["name","resources"],"title":"SearchCoverSchema"},"SearchRomSchema":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"igdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Igdb Id"},"moby_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Moby Id"},"ss_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ss Id"},"sgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sgdb Id"},"flashpoint_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flashpoint Id"},"launchbox_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Launchbox Id"},"platform_id":{"type":"integer","title":"Platform Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug","default":""},"summary":{"type":"string","title":"Summary","default":""},"igdb_url_cover":{"type":"string","title":"Igdb Url Cover","default":""},"moby_url_cover":{"type":"string","title":"Moby Url Cover","default":""},"ss_url_cover":{"type":"string","title":"Ss Url Cover","default":""},"sgdb_url_cover":{"type":"string","title":"Sgdb Url Cover","default":""},"flashpoint_url_cover":{"type":"string","title":"Flashpoint Url Cover","default":""},"launchbox_url_cover":{"type":"string","title":"Launchbox Url Cover","default":""},"is_unidentified":{"type":"boolean","title":"Is Unidentified"},"is_identified":{"type":"boolean","title":"Is Identified"}},"type":"object","required":["platform_id","name","is_unidentified","is_identified"],"title":"SearchRomSchema"},"SiblingRomSchema":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"fs_name_no_tags":{"type":"string","title":"Fs Name No Tags"},"fs_name_no_ext":{"type":"string","title":"Fs Name No Ext"},"sort_comparator":{"type":"string","title":"Sort Comparator","readOnly":true}},"type":"object","required":["id","name","fs_name_no_tags","fs_name_no_ext","sort_comparator"],"title":"SiblingRomSchema"},"SimpleRomSchema":{"properties":{"id":{"type":"integer","title":"Id"},"igdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Igdb Id"},"sgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sgdb Id"},"moby_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Moby Id"},"ss_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ss Id"},"ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ra Id"},"launchbox_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Launchbox Id"},"hasheous_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hasheous Id"},"tgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tgdb Id"},"flashpoint_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flashpoint Id"},"hltb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hltb Id"},"gamelist_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gamelist Id"},"platform_id":{"type":"integer","title":"Platform Id"},"platform_slug":{"type":"string","title":"Platform Slug"},"platform_fs_slug":{"type":"string","title":"Platform Fs Slug"},"platform_custom_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform Custom Name"},"platform_display_name":{"type":"string","title":"Platform Display Name"},"fs_name":{"type":"string","title":"Fs Name"},"fs_name_no_tags":{"type":"string","title":"Fs Name No Tags"},"fs_name_no_ext":{"type":"string","title":"Fs Name No Ext"},"fs_extension":{"type":"string","title":"Fs Extension"},"fs_path":{"type":"string","title":"Fs Path"},"fs_size_bytes":{"type":"integer","title":"Fs Size Bytes"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"alternative_names":{"items":{"type":"string"},"type":"array","title":"Alternative Names"},"youtube_video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Video Id"},"metadatum":{"$ref":"#/components/schemas/RomMetadataSchema"},"igdb_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomIGDBMetadata"},{"type":"null"}]},"moby_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomMobyMetadata"},{"type":"null"}]},"ss_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomSSMetadata"},{"type":"null"}]},"launchbox_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomLaunchboxMetadata"},{"type":"null"}]},"hasheous_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomHasheousMetadata"},{"type":"null"}]},"flashpoint_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomFlashpointMetadata"},{"type":"null"}]},"hltb_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomHLTBMetadata"},{"type":"null"}]},"gamelist_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomGamelistMetadata"},{"type":"null"}]},"manual_metadata":{"anyOf":[{"$ref":"#/components/schemas/ManualMetadata"},{"type":"null"}]},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"url_cover":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Cover"},"has_manual":{"type":"boolean","title":"Has Manual"},"path_manual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Manual"},"url_manual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Manual"},"is_identifying":{"type":"boolean","title":"Is Identifying","default":false},"is_unidentified":{"type":"boolean","title":"Is Unidentified"},"is_identified":{"type":"boolean","title":"Is Identified"},"revision":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revision"},"regions":{"items":{"type":"string"},"type":"array","title":"Regions"},"languages":{"items":{"type":"string"},"type":"array","title":"Languages"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"crc_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crc Hash"},"md5_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5 Hash"},"sha1_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sha1 Hash"},"has_simple_single_file":{"type":"boolean","title":"Has Simple Single File"},"has_nested_single_file":{"type":"boolean","title":"Has Nested Single File"},"has_multiple_files":{"type":"boolean","title":"Has Multiple Files"},"files":{"items":{"$ref":"#/components/schemas/RomFileSchema"},"type":"array","title":"Files"},"full_path":{"type":"string","title":"Full Path"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"has_notes":{"type":"boolean","title":"Has Notes"},"siblings":{"items":{"$ref":"#/components/schemas/SiblingRomSchema"},"type":"array","title":"Siblings"},"rom_user":{"$ref":"#/components/schemas/RomUserSchema"},"merged_screenshots":{"items":{"type":"string"},"type":"array","title":"Merged Screenshots"},"merged_ra_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomRAMetadata"},{"type":"null"}]}},"type":"object","required":["id","igdb_id","sgdb_id","moby_id","ss_id","ra_id","launchbox_id","hasheous_id","tgdb_id","flashpoint_id","hltb_id","gamelist_id","platform_id","platform_slug","platform_fs_slug","platform_custom_name","platform_display_name","fs_name","fs_name_no_tags","fs_name_no_ext","fs_extension","fs_path","fs_size_bytes","name","slug","summary","alternative_names","youtube_video_id","metadatum","igdb_metadata","moby_metadata","ss_metadata","launchbox_metadata","hasheous_metadata","flashpoint_metadata","hltb_metadata","gamelist_metadata","manual_metadata","path_cover_small","path_cover_large","url_cover","has_manual","path_manual","url_manual","is_unidentified","is_identified","revision","regions","languages","tags","crc_hash","md5_hash","sha1_hash","has_simple_single_file","has_nested_single_file","has_multiple_files","files","full_path","created_at","updated_at","missing_from_fs","has_notes","siblings","rom_user","merged_screenshots","merged_ra_metadata"],"title":"SimpleRomSchema"},"SmartCollectionSchema":{"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description","default":""},"rom_ids":{"items":{"type":"integer"},"type":"array","uniqueItems":true,"title":"Rom Ids"},"rom_count":{"type":"integer","title":"Rom Count"},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"path_covers_small":{"items":{"type":"string"},"type":"array","title":"Path Covers Small"},"path_covers_large":{"items":{"type":"string"},"type":"array","title":"Path Covers Large"},"is_public":{"type":"boolean","title":"Is Public","default":false},"is_favorite":{"type":"boolean","title":"Is Favorite","default":false},"is_virtual":{"type":"boolean","title":"Is Virtual","default":false},"is_smart":{"type":"boolean","title":"Is Smart","default":true},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"id":{"type":"integer","title":"Id"},"filter_criteria":{"additionalProperties":true,"type":"object","title":"Filter Criteria"},"filter_summary":{"type":"string","title":"Filter Summary"},"user_id":{"type":"integer","title":"User Id"},"user__username":{"type":"string","title":"User Username"}},"type":"object","required":["name","rom_ids","rom_count","path_cover_small","path_cover_large","path_covers_small","path_covers_large","created_at","updated_at","id","filter_criteria","filter_summary","user_id","user__username"],"title":"SmartCollectionSchema"},"StateSchema":{"properties":{"id":{"type":"integer","title":"Id"},"rom_id":{"type":"integer","title":"Rom Id"},"user_id":{"type":"integer","title":"User Id"},"file_name":{"type":"string","title":"File Name"},"file_name_no_tags":{"type":"string","title":"File Name No Tags"},"file_name_no_ext":{"type":"string","title":"File Name No Ext"},"file_extension":{"type":"string","title":"File Extension"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"download_path":{"type":"string","title":"Download Path"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"emulator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emulator"},"screenshot":{"anyOf":[{"$ref":"#/components/schemas/ScreenshotSchema"},{"type":"null"}]}},"type":"object","required":["id","rom_id","user_id","file_name","file_name_no_tags","file_name_no_ext","file_extension","file_path","file_size_bytes","full_path","download_path","missing_from_fs","created_at","updated_at","emulator","screenshot"],"title":"StateSchema"},"StatsReturn":{"properties":{"PLATFORMS":{"type":"integer","title":"Platforms"},"ROMS":{"type":"integer","title":"Roms"},"SAVES":{"type":"integer","title":"Saves"},"STATES":{"type":"integer","title":"States"},"SCREENSHOTS":{"type":"integer","title":"Screenshots"},"TOTAL_FILESIZE_BYTES":{"type":"integer","title":"Total Filesize Bytes"}},"type":"object","required":["PLATFORMS","ROMS","SAVES","STATES","SCREENSHOTS","TOTAL_FILESIZE_BYTES"],"title":"StatsReturn"},"SystemDict":{"properties":{"VERSION":{"type":"string","title":"Version"},"SHOW_SETUP_WIZARD":{"type":"boolean","title":"Show Setup Wizard"}},"type":"object","required":["VERSION","SHOW_SETUP_WIZARD"],"title":"SystemDict"},"TaskExecutionResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at"],"title":"TaskExecutionResponse"},"TaskInfo":{"properties":{"name":{"type":"string","title":"Name"},"type":{"$ref":"#/components/schemas/TaskType"},"manual_run":{"type":"boolean","title":"Manual Run"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"enabled":{"type":"boolean","title":"Enabled"},"cron_string":{"type":"string","title":"Cron String"}},"type":"object","required":["name","type","manual_run","title","description","enabled","cron_string"],"title":"TaskInfo"},"TaskType":{"type":"string","enum":["scan","conversion","cleanup","update","watcher","generic"],"title":"TaskType","description":"Enumeration of task types for categorization and UI display."},"TasksDict":{"properties":{"ENABLE_SCHEDULED_RESCAN":{"type":"boolean","title":"Enable Scheduled Rescan"},"SCHEDULED_RESCAN_CRON":{"type":"string","title":"Scheduled Rescan Cron"},"ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB":{"type":"boolean","title":"Enable Scheduled Update Switch Titledb"},"SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON":{"type":"string","title":"Scheduled Update Switch Titledb Cron"},"ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA":{"type":"boolean","title":"Enable Scheduled Update Launchbox Metadata"},"SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON":{"type":"string","title":"Scheduled Update Launchbox Metadata Cron"},"ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP":{"type":"boolean","title":"Enable Scheduled Convert Images To Webp"},"SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRON":{"type":"string","title":"Scheduled Convert Images To Webp Cron"}},"type":"object","required":["ENABLE_SCHEDULED_RESCAN","SCHEDULED_RESCAN_CRON","ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB","SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON","ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA","SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON","ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP","SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRON"],"title":"TasksDict"},"TinfoilFeedFileSchema":{"properties":{"url":{"type":"string","title":"Url"},"size":{"type":"integer","title":"Size"}},"type":"object","required":["url","size"],"title":"TinfoilFeedFileSchema"},"TinfoilFeedSchema":{"properties":{"files":{"items":{"$ref":"#/components/schemas/TinfoilFeedFileSchema"},"type":"array","title":"Files"},"directories":{"items":{"type":"string"},"type":"array","title":"Directories"},"titledb":{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object","title":"Titledb"},"success":{"type":"string","title":"Success"},"error":{"type":"string","title":"Error"}},"type":"object","required":["files","directories"],"title":"TinfoilFeedSchema"},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"token_type":{"type":"string","title":"Token Type"},"expires":{"type":"integer","title":"Expires"}},"type":"object","required":["access_token","token_type","expires"],"title":"TokenResponse"},"UpdateStats":{"properties":{"processed":{"type":"integer","title":"Processed"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["processed","total"],"title":"UpdateStats"},"UpdateTaskMeta":{"properties":{"update_stats":{"anyOf":[{"$ref":"#/components/schemas/UpdateStats"},{"type":"null"}]}},"type":"object","required":["update_stats"],"title":"UpdateTaskMeta"},"UpdateTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"update","title":"Task Type"},"meta":{"$ref":"#/components/schemas/UpdateTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"UpdateTaskStatusResponse"},"UserCollectionSchema":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["id","name"],"title":"UserCollectionSchema"},"UserForm":{"properties":{"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"},"ra_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ra Username"},"avatar":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Avatar"},"ui_settings":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ui Settings"}},"type":"object","title":"UserForm"},"UserNoteSchema":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"type":"string","title":"Title"},"content":{"type":"string","title":"Content"},"is_public":{"type":"boolean","title":"Is Public"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"user_id":{"type":"integer","title":"User Id"},"username":{"type":"string","title":"Username"}},"type":"object","required":["id","title","content","is_public","created_at","updated_at","user_id","username"],"title":"UserNoteSchema"},"UserSchema":{"properties":{"id":{"type":"integer","title":"Id"},"username":{"type":"string","title":"Username"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"enabled":{"type":"boolean","title":"Enabled"},"role":{"$ref":"#/components/schemas/Role"},"oauth_scopes":{"items":{"type":"string"},"type":"array","title":"Oauth Scopes"},"avatar_path":{"type":"string","title":"Avatar Path"},"last_login":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Login"},"last_active":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Active"},"ra_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ra Username"},"ra_progression":{"anyOf":[{"$ref":"#/components/schemas/RAProgression"},{"type":"null"}]},"ui_settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Ui Settings"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","username","email","enabled","role","oauth_scopes","avatar_path","last_login","last_active","created_at","updated_at"],"title":"UserSchema"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VirtualCollectionSchema":{"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"rom_ids":{"items":{"type":"integer"},"type":"array","uniqueItems":true,"title":"Rom Ids"},"rom_count":{"type":"integer","title":"Rom Count"},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"path_covers_small":{"items":{"type":"string"},"type":"array","title":"Path Covers Small"},"path_covers_large":{"items":{"type":"string"},"type":"array","title":"Path Covers Large"},"is_public":{"type":"boolean","title":"Is Public","default":false},"is_favorite":{"type":"boolean","title":"Is Favorite","default":false},"is_virtual":{"type":"boolean","title":"Is Virtual","default":true},"is_smart":{"type":"boolean","title":"Is Smart","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"id":{"type":"string","title":"Id"},"type":{"type":"string","title":"Type"}},"type":"object","required":["name","description","rom_ids","rom_count","path_cover_small","path_cover_large","path_covers_small","path_covers_large","created_at","updated_at","id","type"],"title":"VirtualCollectionSchema"},"WatcherTaskMeta":{"properties":{},"type":"object","title":"WatcherTaskMeta"},"WatcherTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"watcher","title":"Task Type"},"meta":{"$ref":"#/components/schemas/WatcherTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"WatcherTaskStatusResponse"},"WebrcadeFeedCategorySchema":{"properties":{"title":{"type":"string","title":"Title"},"longTitle":{"type":"string","title":"Longtitle"},"background":{"type":"string","title":"Background"},"thumbnail":{"type":"string","title":"Thumbnail"},"description":{"type":"string","title":"Description"},"items":{"items":{"$ref":"#/components/schemas/WebrcadeFeedItemSchema"},"type":"array","title":"Items"}},"type":"object","required":["title","items"],"title":"WebrcadeFeedCategorySchema"},"WebrcadeFeedItemPropsSchema":{"properties":{"rom":{"type":"string","title":"Rom"}},"type":"object","required":["rom"],"title":"WebrcadeFeedItemPropsSchema"},"WebrcadeFeedItemSchema":{"properties":{"title":{"type":"string","title":"Title"},"longTitle":{"type":"string","title":"Longtitle"},"description":{"type":"string","title":"Description"},"type":{"type":"string","title":"Type"},"thumbnail":{"type":"string","title":"Thumbnail"},"background":{"type":"string","title":"Background"},"props":{"$ref":"#/components/schemas/WebrcadeFeedItemPropsSchema"}},"type":"object","required":["title","type","props"],"title":"WebrcadeFeedItemSchema"},"WebrcadeFeedSchema":{"properties":{"title":{"type":"string","title":"Title"},"longTitle":{"type":"string","title":"Longtitle"},"description":{"type":"string","title":"Description"},"thumbnail":{"type":"string","title":"Thumbnail"},"background":{"type":"string","title":"Background"},"categories":{"items":{"$ref":"#/components/schemas/WebrcadeFeedCategorySchema"},"type":"array","title":"Categories"}},"type":"object","required":["title","categories"],"title":"WebrcadeFeedSchema"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{"me.read":"View your profile","roms.read":"View ROMs","platforms.read":"View platforms","assets.read":"View assets","firmware.read":"View firmware","roms.user.read":"View user-rom properties","collections.read":"View collections","me.write":"Modify your profile","assets.write":"Modify assets","roms.user.write":"Modify user-rom properties","collections.write":"Modify collections","roms.write":"Modify ROMs","platforms.write":"Modify platforms","firmware.write":"Modify firmware","users.read":"View users","users.write":"Modify users","tasks.run":"Run tasks"},"tokenUrl":"/token"}}},"HTTPBasic":{"type":"http","scheme":"basic"}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"RomM API","version":"4.7.0"},"paths":{"/api/heartbeat":{"get":{"tags":["system"],"summary":"Heartbeat","description":"Endpoint to set the CSRF token in cache and return all the basic RomM config\n\nReturns:\n HeartbeatReturn: TypedDict structure with all the defined values in the HeartbeatReturn class.","operationId":"heartbeat_api_heartbeat_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatResponse"}}}}}}},"/api/heartbeat/metadata/{source}":{"get":{"tags":["system"],"summary":"Metadata Heartbeat","description":"Endpoint to return the heartbeat of the metadata sources","operationId":"metadata_heartbeat_api_heartbeat_metadata__source__get","parameters":[{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Metadata Heartbeat Api Heartbeat Metadata Source Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/setup/library":{"get":{"tags":["system"],"summary":"Get Setup Library Info","description":"Get library structure information for setup wizard.\n\nOnly accessible during initial setup (no admin users) or with authentication.\n\nReturns:\n - detected_structure: \"struct_a\" (roms/{platform}), \"struct_b\" ({platform}/roms), or None\n - existing_platforms: list of objects with fs_slug and rom_count\n - supported_platforms: list of all supported platforms with metadata","operationId":"get_setup_library_info_api_setup_library_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}]}},"/api/setup/platforms":{"post":{"tags":["system"],"summary":"Create Setup Platforms","description":"Create platform folders during setup wizard.\n\nOnly accessible during initial setup (no admin users) or with authentication.\n\nArgs:\n platform_slugs: List of platform fs_slugs to create\n\nReturns:\n - success: bool\n - created_count: number of platforms created\n - message: success or error message","operationId":"create_setup_platforms_api_setup_platforms_post","requestBody":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Platform Slugs"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}]}},"/api/login":{"post":{"tags":["auth"],"summary":"Login","description":"Session login endpoint\n\nArgs:\n request (Request): Fastapi Request object\n credentials: Defaults to Depends(HTTPBasic()).\n\nRaises:\n CredentialsException: Invalid credentials\n UserDisabledException: Auth is disabled","operationId":"login_api_login_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBasic":[]}]}},"/api/logout":{"post":{"tags":["auth"],"summary":"Logout","description":"Session logout endpoint\n\nArgs:\n request (Request): Fastapi Request object","operationId":"logout_api_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/token":{"post":{"tags":["auth"],"summary":"Token","description":"OAuth2 token endpoint\n\nArgs:\n form_data (Annotated[OAuth2RequestForm, Depends): Form Data with OAuth2 info\n\nRaises:\n HTTPException: Missing refresh token\n HTTPException: Invalid refresh token\n HTTPException: Missing username or password\n HTTPException: Invalid username or password\n HTTPException: Client credentials are not yet supported\n HTTPException: Invalid or unsupported grant type\n HTTPException: Insufficient scope\n\nReturns:\n TokenResponse: TypedDict with the new generated token info","operationId":"token_api_token_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_token_api_token_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/login/openid":{"get":{"tags":["auth"],"summary":"Login Via Openid","description":"OIDC login endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nRaises:\n OIDCDisabledException: OAuth is disabled\n OIDCNotConfiguredException: OAuth not configured\n\nReturns:\n RedirectResponse: Redirect to OIDC provider","operationId":"login_via_openid_api_login_openid_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/oauth/openid":{"get":{"tags":["auth"],"summary":"Auth Openid","description":"OIDC callback endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nRaises:\n OIDCDisabledException: OAuth is disabled\n OIDCNotConfiguredException: OAuth not configured\n AuthCredentialsException: Invalid credentials\n UserDisabledException: Auth is disabled\n\nReturns:\n RedirectResponse: Redirect to home page","operationId":"auth_openid_api_oauth_openid_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/forgot-password":{"post":{"tags":["auth"],"summary":"Request Password Reset","description":"Request a password reset link for the user.\n\nArgs:\n username (str): Username of the user requesting the reset\nReturns:\n None: Returns 200 OK status","operationId":"request_password_reset_api_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_request_password_reset_api_forgot_password_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reset-password":{"post":{"tags":["auth"],"summary":"Reset Password","description":"Reset password using the token.\n\nArgs:\n token (str): Reset token from the URL\n new_password (str): New user password\n\nReturns:\n None: Returns 200 OK status","operationId":"reset_password_api_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_reset_password_api_reset_password_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/users":{"get":{"tags":["users"],"summary":"Get Users","description":"Get all users endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[UserSchema]: All users stored in the RomM's database","operationId":"get_users_api_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/UserSchema"},"type":"array","title":"Response Get Users Api Users Get"}}}}},"security":[{"OAuth2PasswordBearer":["users.read"]},{"HTTPBasic":[]}]},"post":{"tags":["users"],"summary":"Add User","description":"Create user endpoint\n\nArgs:\n request (Request): Fastapi Requests object\n username (str): User username\n password (str): User password\n email (str): User email\n role (str): RomM Role object represented as string\n\nReturns:\n UserSchema: Newly created user","operationId":"add_user_api_users_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_add_user_api_users_post"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}]}},"/api/users/invite-link":{"post":{"tags":["users"],"summary":"Create Invite Link","description":"Create an invite link for a user.\n\nArgs:\n request (Request): FastAPI Request object\n role (str): The role of the user\n\nReturns:\n InviteLinkSchema: Invite link","operationId":"create_invite_link_api_users_invite_link_post","security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}],"parameters":[{"name":"role","in":"query","required":true,"schema":{"type":"string","title":"Role"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteLinkSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/users/register":{"post":{"tags":["users"],"summary":"Create User From Invite","description":"Create user endpoint with invite link\n\nArgs:\n username (str): User username\n email (str): User email\n password (str): User password\n token (str): Invite link token\n\nReturns:\n UserSchema: Newly created user","operationId":"create_user_from_invite_api_users_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_create_user_from_invite_api_users_register_post"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/users/identifiers":{"get":{"tags":["users"],"summary":"Get User Identifiers","description":"Get all user identifiers endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[int]: All user ids stored in the RomM's database","operationId":"get_user_identifiers_api_users_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Get User Identifiers Api Users Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["users.read"]},{"HTTPBasic":[]}]}},"/api/users/me":{"get":{"tags":["users"],"summary":"Get Current User","description":"Get current user endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n UserSchema | None: Current user","operationId":"get_current_user_api_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/UserSchema"},{"type":"null"}],"title":"Response Get Current User Api Users Me Get"}}}}},"security":[{"OAuth2PasswordBearer":["me.read"]},{"HTTPBasic":[]}]}},"/api/users/{id}":{"get":{"tags":["users"],"summary":"Get User","description":"Get user endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n UserSchem: User stored in the RomM's database","operationId":"get_user_api_users__id__get","security":[{"OAuth2PasswordBearer":["users.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["users"],"summary":"Update User","description":"Update user endpoint\n\nArgs:\n request (Request): Fastapi Requests object\n user_id (int): User internal id\n form_data (Annotated[UserUpdateForm, Depends): Form Data with user updated info\n\nRaises:\n HTTPException: User is not found in database\n HTTPException: Username already in use by another user\n\nReturns:\n UserSchema: Updated user info","operationId":"update_user_api_users__id__put","security":[{"OAuth2PasswordBearer":["me.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/UserForm"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["users"],"summary":"Delete User","description":"Delete a user by ID.\n\nRaises:\n HTTPException: User is not found in database\n HTTPException: User deleting itself\n HTTPException: User is the last admin user","operationId":"delete_user_api_users__id__delete","security":[{"OAuth2PasswordBearer":["users.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"User internal id.","title":"Id"},"description":"User internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad Request"},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/users/{id}/ra/refresh":{"post":{"tags":["users"],"summary":"Refresh RetroAchievements","description":"Refresh RetroAchievements progression data for a user.","operationId":"refresh_retro_achievements_api_users__id__ra_refresh_post","security":[{"OAuth2PasswordBearer":["me.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"User internal id.","title":"Id"},"description":"User internal id."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_refresh_retro_achievements_api_users__id__ra_refresh_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/devices":{"get":{"tags":["devices"],"summary":"Get Devices","operationId":"get_devices_api_devices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/DeviceSchema"},"type":"array","title":"Response Get Devices Api Devices Get"}}}}},"security":[{"OAuth2PasswordBearer":["devices.read"]},{"HTTPBasic":[]}]},"post":{"tags":["devices"],"summary":"Register Device","operationId":"register_device_api_devices_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceCreatePayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["devices.write"]},{"HTTPBasic":[]}]}},"/api/devices/{device_id}":{"get":{"tags":["devices"],"summary":"Get Device","operationId":"get_device_api_devices__device_id__get","security":[{"OAuth2PasswordBearer":["devices.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["devices"],"summary":"Update Device","operationId":"update_device_api_devices__device_id__put","security":[{"OAuth2PasswordBearer":["devices.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceUpdatePayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["devices"],"summary":"Delete Device","operationId":"delete_device_api_devices__device_id__delete","security":[{"OAuth2PasswordBearer":["devices.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platforms":{"post":{"tags":["platforms"],"summary":"Add Platform","description":"Create a platform.","operationId":"add_platform_api_platforms_post","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_add_platform_api_platforms_post"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["platforms"],"summary":"Get Platforms","description":"Retrieve platforms.","operationId":"get_platforms_api_platforms_get","security":[{"OAuth2PasswordBearer":["platforms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"updated_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter platforms updated after this datetime (ISO 8601 format with timezone information).","title":"Updated After"},"description":"Filter platforms updated after this datetime (ISO 8601 format with timezone information)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PlatformSchema"},"title":"Response Get Platforms Api Platforms Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platforms/identifiers":{"get":{"tags":["platforms"],"summary":"Get Platform Identifiers","description":"Retrieve platform identifiers.","operationId":"get_platform_identifiers_api_platforms_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Get Platform Identifiers Api Platforms Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["platforms.read"]},{"HTTPBasic":[]}]}},"/api/platforms/supported":{"get":{"tags":["platforms"],"summary":"Get Supported Platforms Endpoint","description":"Retrieve the list of supported platforms.","operationId":"get_supported_platforms_endpoint_api_platforms_supported_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PlatformSchema"},"type":"array","title":"Response Get Supported Platforms Endpoint Api Platforms Supported Get"}}}}},"security":[{"OAuth2PasswordBearer":["platforms.read"]},{"HTTPBasic":[]}]}},"/api/platforms/{id}":{"get":{"tags":["platforms"],"summary":"Get Platform","description":"Retrieve a platform by ID.","operationId":"get_platform_api_platforms__id__get","security":[{"OAuth2PasswordBearer":["platforms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Platform id.","title":"Id"},"description":"Platform id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["platforms"],"summary":"Update Platform","description":"Update a platform.","operationId":"update_platform_api_platforms__id__put","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Platform id.","title":"Id"},"description":"Platform id."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_update_platform_api_platforms__id__put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["platforms"],"summary":"Delete Platform","description":"Delete a platform by ID.","operationId":"delete_platform_api_platforms__id__delete","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Platform id.","title":"Id"},"description":"Platform id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms":{"post":{"tags":["roms"],"summary":"Add Rom","description":"Upload a single rom.","operationId":"add_rom_api_roms_post","security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"x-upload-platform","in":"header","required":true,"schema":{"type":"integer","minimum":1,"description":"Platform internal id.","title":"X-Upload-Platform"},"description":"Platform internal id."},{"name":"x-upload-filename","in":"header","required":true,"schema":{"type":"string","description":"The name of the file being uploaded.","title":"X-Upload-Filename"},"description":"The name of the file being uploaded."}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad Request"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["roms"],"summary":"Get Roms","description":"Retrieve roms.","operationId":"get_roms_api_roms_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"with_char_index","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to get the char index.","default":true,"title":"With Char Index"},"description":"Whether to get the char index."},{"name":"with_filter_values","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to return filter values.","default":true,"title":"With Filter Values"},"description":"Whether to return filter values."},{"name":"search_term","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search term to filter roms.","title":"Search Term"},"description":"Search term to filter roms."},{"name":"platform_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"integer"}},{"type":"null"}],"description":"Platform internal ids. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Platform Ids"},"description":"Platform internal ids. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"collection_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Collection internal id.","title":"Collection Id"},"description":"Collection internal id."},{"name":"virtual_collection_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Virtual collection internal id.","title":"Virtual Collection Id"},"description":"Virtual collection internal id."},{"name":"smart_collection_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Smart collection internal id.","title":"Smart Collection Id"},"description":"Smart collection internal id."},{"name":"matched","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom matched at least one metadata source.","title":"Matched"},"description":"Whether the rom matched at least one metadata source."},{"name":"favorite","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is marked as favorite.","title":"Favorite"},"description":"Whether the rom is marked as favorite."},{"name":"duplicate","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is marked as duplicate.","title":"Duplicate"},"description":"Whether the rom is marked as duplicate."},{"name":"last_played","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom has a last played value for the current user.","title":"Last Played"},"description":"Whether the rom has a last played value for the current user."},{"name":"playable","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is playable from the browser.","title":"Playable"},"description":"Whether the rom is playable from the browser."},{"name":"missing","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is missing from the filesystem.","title":"Missing"},"description":"Whether the rom is missing from the filesystem."},{"name":"has_ra","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom has RetroAchievements data.","title":"Has Ra"},"description":"Whether the rom has RetroAchievements data."},{"name":"verified","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the rom is verified by Hasheous.","title":"Verified"},"description":"Whether the rom is verified by Hasheous."},{"name":"group_by_meta_id","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to group roms by metadata ID (IGDB / Moby / ScreenScraper / RetroAchievements / LaunchBox).","default":false,"title":"Group By Meta Id"},"description":"Whether to group roms by metadata ID (IGDB / Moby / ScreenScraper / RetroAchievements / LaunchBox)."},{"name":"genres","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated genre. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Genres"},"description":"Associated genre. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"franchises","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated franchise. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Franchises"},"description":"Associated franchise. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"collections","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated collection. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Collections"},"description":"Associated collection. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"companies","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated company. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Companies"},"description":"Associated company. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"age_ratings","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated age rating. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Age Ratings"},"description":"Associated age rating. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"statuses","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Game status, set by the current user. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Statuses"},"description":"Game status, set by the current user. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"regions","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated region tag. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Regions"},"description":"Associated region tag. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"languages","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated language tag. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Languages"},"description":"Associated language tag. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"player_counts","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Associated player count. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned.","title":"Player Counts"},"description":"Associated player count. Multiple values are allowed by repeating the parameter, and results that match any of the values will be returned."},{"name":"genres_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for genres filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Genres Logic"},"description":"Logic operator for genres filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"franchises_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for franchises filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Franchises Logic"},"description":"Logic operator for franchises filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"collections_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for collections filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Collections Logic"},"description":"Logic operator for collections filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"companies_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for companies filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Companies Logic"},"description":"Logic operator for companies filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"age_ratings_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for age ratings filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Age Ratings Logic"},"description":"Logic operator for age ratings filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"regions_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for regions filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Regions Logic"},"description":"Logic operator for regions filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"languages_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for languages filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Languages Logic"},"description":"Logic operator for languages filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"statuses_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for statuses filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Statuses Logic"},"description":"Logic operator for statuses filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"player_counts_logic","in":"query","required":false,"schema":{"type":"string","description":"Logic operator for player counts filter: 'any' (OR), 'all' (AND) or 'none' (NOT).","default":"any","title":"Player Counts Logic"},"description":"Logic operator for player counts filter: 'any' (OR), 'all' (AND) or 'none' (NOT)."},{"name":"order_by","in":"query","required":false,"schema":{"type":"string","description":"Field to order results by.","default":"name","title":"Order By"},"description":"Field to order results by."},{"name":"order_dir","in":"query","required":false,"schema":{"type":"string","description":"Order direction, either 'asc' or 'desc'.","default":"asc","title":"Order Dir"},"description":"Order direction, either 'asc' or 'desc'."},{"name":"updated_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter roms updated after this datetime (ISO 8601 format with timezone information).","title":"Updated After"},"description":"Filter roms updated after this datetime (ISO 8601 format with timezone information)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Page size limit","default":50,"title":"Limit"},"description":"Page size limit"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Page offset","default":0,"title":"Offset"},"description":"Page offset"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomLimitOffsetPage_SimpleRomSchema_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/identifiers":{"get":{"tags":["roms"],"summary":"Get Rom Identifiers","description":"Retrieve rom identifiers.","operationId":"get_rom_identifiers_api_roms_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Get Rom Identifiers Api Roms Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/roms/download":{"get":{"tags":["roms"],"summary":"Download Roms","description":"Download a list of roms as a zip file.","operationId":"download_roms_api_roms_download_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_ids","in":"query","required":true,"schema":{"type":"string","description":"Comma-separated list of ROM IDs to download as a zip file.","title":"Rom Ids"},"description":"Comma-separated list of ROM IDs to download as a zip file."},{"name":"filename","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Name for the zip file (optional).","title":"Filename"},"description":"Name for the zip file (optional)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/by-metadata-provider":{"get":{"tags":["roms"],"summary":"Get Rom By Metadata Provider","description":"Retrieve a rom by metadata ID.","operationId":"get_rom_by_metadata_provider_api_roms_by_metadata_provider_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"igdb_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"IGDB ID to search by","title":"Igdb Id"},"description":"IGDB ID to search by"},{"name":"moby_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"MobyGames ID to search by","title":"Moby Id"},"description":"MobyGames ID to search by"},{"name":"ss_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"ScreenScraper ID to search by","title":"Ss Id"},"description":"ScreenScraper ID to search by"},{"name":"ra_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"RetroAchievements ID to search by","title":"Ra Id"},"description":"RetroAchievements ID to search by"},{"name":"launchbox_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"LaunchBox ID to search by","title":"Launchbox Id"},"description":"LaunchBox ID to search by"},{"name":"hasheous_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Hasheous ID to search by","title":"Hasheous Id"},"description":"Hasheous ID to search by"},{"name":"tgdb_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"TGDB ID to search by","title":"Tgdb Id"},"description":"TGDB ID to search by"},{"name":"flashpoint_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Flashpoint ID to search by","title":"Flashpoint Id"},"description":"Flashpoint ID to search by"},{"name":"hltb_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"HLTB ID to search by","title":"Hltb Id"},"description":"HLTB ID to search by"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailedRomSchema"}}}},"404":{"description":"Not Found"},"400":{"description":"Bad Request"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/by-hash":{"get":{"tags":["roms"],"summary":"Get Rom By Hash","operationId":"get_rom_by_hash_api_roms_by_hash_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"crc_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CRC hash value","title":"Crc Hash"},"description":"CRC hash value"},{"name":"md5_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"MD5 hash value","title":"Md5 Hash"},"description":"MD5 hash value"},{"name":"sha1_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"SHA1 hash value","title":"Sha1 Hash"},"description":"SHA1 hash value"},{"name":"ra_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"RetroAchievements hash value","title":"Ra Hash"},"description":"RetroAchievements hash value"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailedRomSchema"}}}},"404":{"description":"Not Found"},"400":{"description":"Bad Request"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/filters":{"get":{"tags":["roms"],"summary":"Get Rom Filters","operationId":"get_rom_filters_api_roms_filters_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RomFiltersDict"}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/roms/{id}":{"get":{"tags":["roms"],"summary":"Get Rom","description":"Retrieve a rom by ID.","operationId":"get_rom_api_roms__id__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailedRomSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["roms"],"summary":"Update Rom","description":"Update a rom.","operationId":"update_rom_api_roms__id__put","security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"remove_cover","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to remove the cover image for this rom.","default":false,"title":"Remove Cover"},"description":"Whether to remove the cover image for this rom."},{"name":"unmatch_metadata","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to remove the metadata matches for this game.","default":false,"title":"Unmatch Metadata"},"description":"Whether to remove the metadata matches for this game."}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_update_rom_api_roms__id__put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailedRomSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/content/{file_name}":{"head":{"tags":["roms"],"summary":"Head Rom Content","description":"Retrieve head information for a rom file download.","operationId":"head_rom_content_api_roms__id__content__file_name__head","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","description":"File name to download","title":"File Name"},"description":"File name to download"},{"name":"file_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated list of file ids to download for multi-part roms.","title":"File Ids"},"description":"Comma-separated list of file ids to download for multi-part roms."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["roms"],"summary":"Get Rom Content","description":"Download a rom.\n\nThis endpoint serves the content of the requested rom, as:\n- A single file for single file roms.\n- A zipped file for multi-part roms, including a .m3u file if applicable.","operationId":"get_rom_content_api_roms__id__content__file_name__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","description":"Zip file output name","title":"File Name"},"description":"Zip file output name"},{"name":"file_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated list of file ids to download for multi-part roms.","title":"File Ids"},"description":"Comma-separated list of file ids to download for multi-part roms."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/manuals":{"post":{"tags":["roms"],"summary":"Add Rom Manuals","description":"Upload manuals for a rom.","operationId":"add_rom_manuals_api_roms__id__manuals_post","security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"x-upload-filename","in":"header","required":true,"schema":{"type":"string","description":"The name of the file being uploaded.","title":"X-Upload-Filename"},"description":"The name of the file being uploaded."}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["roms"],"summary":"Delete Rom Manuals","description":"Delete manuals for a rom.","operationId":"delete_rom_manuals_api_roms__id__manuals_delete","security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/delete":{"post":{"tags":["roms"],"summary":"Delete Roms","description":"Delete roms.","operationId":"delete_roms_api_roms_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_delete_roms_api_roms_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkOperationResponse"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["roms.write"]},{"HTTPBasic":[]}]}},"/api/roms/{id}/props":{"put":{"tags":["roms"],"summary":"Update Rom User","description":"Update rom data associated to the current user.","operationId":"update_rom_user_api_roms__id__props_put","security":[{"OAuth2PasswordBearer":["roms.user.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_update_rom_user_api_roms__id__props_put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RomUserSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/files/{id}":{"get":{"tags":["roms"],"summary":"Get Romfile","description":"Retrieve a rom file by ID.","operationId":"get_romfile_api_roms_files__id__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom file internal id.","title":"Id"},"description":"Rom file internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RomFileSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/romsfiles/{id}/content/{file_name}":{"get":{"tags":["roms"],"summary":"Get Romfile Content","description":"Download a rom file.","operationId":"get_romfile_content_api_romsfiles__id__content__file_name__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom file internal id.","title":"Id"},"description":"Rom file internal id."},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","description":"File name to download","title":"File Name"},"description":"File name to download"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/notes":{"get":{"tags":["roms"],"summary":"Get Rom Notes","description":"Get all notes for a ROM.","operationId":"get_rom_notes_api_roms__id__notes_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"public_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return public notes","default":false,"title":"Public Only"},"description":"Only return public notes"},{"name":"search","in":"query","required":false,"schema":{"type":"string","description":"Search notes by title or content","title":"Search"},"description":"Search notes by title or content"},{"name":"tags","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Filter by tags","title":"Tags"},"description":"Filter by tags"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserNoteSchema"},"title":"Response Get Rom Notes Api Roms Id Notes Get"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["roms"],"summary":"Create Rom Note","description":"Create a new note for a ROM.","operationId":"create_rom_note_api_roms__id__notes_post","security":[{"OAuth2PasswordBearer":["roms.user.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Note Data"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserNoteSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/notes/identifiers":{"get":{"tags":["roms"],"summary":"Get Rom Note Identifiers","description":"Get all note identifiers for a ROM.","operationId":"get_rom_note_identifiers_api_roms__id__notes_identifiers_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"integer"},"title":"Response Get Rom Note Identifiers Api Roms Id Notes Identifiers Get"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roms/{id}/notes/{note_id}":{"put":{"tags":["roms"],"summary":"Update Rom Note","description":"Update a ROM note.","operationId":"update_rom_note_api_roms__id__notes__note_id__put","security":[{"OAuth2PasswordBearer":["roms.user.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"note_id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Note id.","title":"Note Id"},"description":"Note id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Note Data"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserNoteSchema"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["roms"],"summary":"Delete Rom Note","description":"Delete a ROM note.","operationId":"delete_rom_note_api_roms__id__notes__note_id__delete","security":[{"OAuth2PasswordBearer":["roms.user.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Rom internal id.","title":"Id"},"description":"Rom internal id."},{"name":"note_id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Note id.","title":"Note Id"},"description":"Note id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Rom Note Api Roms Id Notes Note Id Delete"}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/search/roms":{"get":{"tags":["search"],"summary":"Search Rom","description":"Search for rom in metadata providers\n\nArgs:\n request (Request): FastAPI request\n rom_id (int): Rom ID\n source (str): Source of the rom\n search_term (str, optional): Search term. Defaults to None.\n search_by (str, optional): Search by name or ID. Defaults to \"name\".\n search_extended (bool, optional): Search extended info. Defaults to False.\n\nReturns:\n list[SearchRomSchema]: List of matched roms","operationId":"search_rom_api_search_roms_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}},{"name":"search_term","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search Term"}},{"name":"search_by","in":"query","required":false,"schema":{"type":"string","default":"name","title":"Search By"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchRomSchema"},"title":"Response Search Rom Api Search Roms Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/search/cover":{"get":{"tags":["search"],"summary":"Search Cover","operationId":"search_cover_api_search_cover_get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"search_term","in":"query","required":false,"schema":{"type":"string","default":"","title":"Search Term"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchCoverSchema"},"title":"Response Search Cover Api Search Cover Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves":{"post":{"tags":["saves"],"summary":"Add Save","description":"Upload a save file for a ROM.","operationId":"add_save_api_saves_post","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}},{"name":"emulator","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emulator"}},{"name":"slot","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slot"}},{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Id"}},{"name":"overwrite","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Overwrite"}},{"name":"autocleanup","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Autocleanup"}},{"name":"autocleanup_limit","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Autocleanup Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["saves"],"summary":"Get Saves","description":"Retrieve saves for the current user.","operationId":"get_saves_api_saves_get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rom Id"}},{"name":"platform_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Platform Id"}},{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Id"}},{"name":"slot","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slot"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SaveSchema"},"title":"Response Get Saves Api Saves Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves/identifiers":{"get":{"tags":["saves"],"summary":"Get Save Identifiers","description":"Retrieve save identifiers.","operationId":"get_save_identifiers_api_saves_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Get Save Identifiers Api Saves Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}]}},"/api/saves/summary":{"get":{"tags":["saves"],"summary":"Get Saves Summary","description":"Retrieve saves summary grouped by slot.","operationId":"get_saves_summary_api_saves_summary_get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSummarySchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves/{id}":{"get":{"tags":["saves"],"summary":"Get Save","description":"Retrieve a save by ID.","operationId":"get_save_api_saves__id__get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["saves"],"summary":"Update Save","description":"Update a save file.","operationId":"update_save_api_saves__id__put","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves/{id}/content":{"get":{"tags":["saves"],"summary":"Download Save","description":"Download a save file.","operationId":"download_save_api_saves__id__content_get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Id"}},{"name":"optimistic","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Optimistic"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves/{id}/downloaded":{"post":{"tags":["saves"],"summary":"Confirm Download","description":"Confirm a save was downloaded successfully.","operationId":"confirm_download_api_saves__id__downloaded_post","security":[{"OAuth2PasswordBearer":["devices.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_confirm_download_api_saves__id__downloaded_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves/delete":{"post":{"tags":["saves"],"summary":"Delete Saves","description":"Delete saves.","operationId":"delete_saves_api_saves_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_delete_saves_api_saves_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Delete Saves Api Saves Delete Post"}}}},"400":{"description":"Bad Request"},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}]}},"/api/saves/{id}/track":{"post":{"tags":["saves"],"summary":"Track Save","description":"Re-enable sync tracking for a save on a device.","operationId":"track_save_api_saves__id__track_post","security":[{"OAuth2PasswordBearer":["devices.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_track_save_api_saves__id__track_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/saves/{id}/untrack":{"post":{"tags":["saves"],"summary":"Untrack Save","description":"Disable sync tracking for a save on a device.","operationId":"untrack_save_api_saves__id__untrack_post","security":[{"OAuth2PasswordBearer":["devices.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_untrack_save_api_saves__id__untrack_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/states":{"post":{"tags":["states"],"summary":"Add State","operationId":"add_state_api_states_post","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}},{"name":"emulator","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emulator"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StateSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["states"],"summary":"Get States","operationId":"get_states_api_states_get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rom Id"}},{"name":"platform_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Platform Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StateSchema"},"title":"Response Get States Api States Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/states/identifiers":{"get":{"tags":["states"],"summary":"Get State Identifiers","description":"Get state identifiers endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[int]: List of state IDs","operationId":"get_state_identifiers_api_states_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Get State Identifiers Api States Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}]}},"/api/states/{id}":{"get":{"tags":["states"],"summary":"Get State","operationId":"get_state_api_states__id__get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StateSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["states"],"summary":"Update State","operationId":"update_state_api_states__id__put","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StateSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/states/delete":{"post":{"tags":["states"],"summary":"Delete States","description":"Delete states.","operationId":"delete_states_api_states_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_delete_states_api_states_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Delete States Api States Delete Post"}}}},"400":{"description":"Bad Request"},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}]}},"/api/tasks":{"get":{"tags":["tasks"],"summary":"List Tasks","description":"List all available tasks grouped by task type.\n\nArgs:\n request (Request): FastAPI Request object\nReturns:\n GroupedTasksDict: Dictionary with tasks grouped by their type (scheduled, manual, watcher)","operationId":"list_tasks_api_tasks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"items":{"$ref":"#/components/schemas/TaskInfo"},"type":"array"},"type":"object","title":"Response List Tasks Api Tasks Get"}}}}},"security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}]}},"/api/tasks/status":{"get":{"tags":["tasks"],"summary":"Get Tasks Status","description":"Get all active, queued, completed, and failed tasks.\n\nArgs:\n request (Request): FastAPI Request object\nReturns:\n list[TaskStatusResponse]: List of all tasks with their current status","operationId":"get_tasks_status_api_tasks_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"anyOf":[{"$ref":"#/components/schemas/ScanTaskStatusResponse"},{"$ref":"#/components/schemas/ConversionTaskStatusResponse"},{"$ref":"#/components/schemas/UpdateTaskStatusResponse"},{"$ref":"#/components/schemas/CleanupTaskStatusResponse"},{"$ref":"#/components/schemas/WatcherTaskStatusResponse"},{"$ref":"#/components/schemas/GenericTaskStatusResponse"}]},"type":"array","title":"Response Get Tasks Status Api Tasks Status Get"}}}}},"security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}]}},"/api/tasks/{task_id}":{"get":{"tags":["tasks"],"summary":"Get Task By Id","description":"Get the status of a task by its job ID.\n\nArgs:\n request (Request): FastAPI Request object\n task_id (str): Job ID of the task to retrieve status for\nReturns:\n TaskStatusResponse: Task status information","operationId":"get_task_by_id_api_tasks__task_id__get","security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}],"parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ScanTaskStatusResponse"},{"$ref":"#/components/schemas/ConversionTaskStatusResponse"},{"$ref":"#/components/schemas/UpdateTaskStatusResponse"},{"$ref":"#/components/schemas/CleanupTaskStatusResponse"},{"$ref":"#/components/schemas/WatcherTaskStatusResponse"},{"$ref":"#/components/schemas/GenericTaskStatusResponse"}],"title":"Response Get Task By Id Api Tasks Task Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks/run":{"post":{"tags":["tasks"],"summary":"Run All Tasks","description":"Run all runnable tasks endpoint\n\nArgs:\n request (Request): FastAPI Request object\nReturns:\n TaskExecutionResponse: Task execution response with details","operationId":"run_all_tasks_api_tasks_run_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TaskExecutionResponse"},"type":"array","title":"Response Run All Tasks Api Tasks Run Post"}}}}},"security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}]}},"/api/tasks/run/{task_name}":{"post":{"tags":["tasks"],"summary":"Run Single Task","description":"Run a single task endpoint.\n\nArgs:\n request (Request): FastAPI Request object\n task_name (str): Name of the task to run\nReturns:\n TaskExecutionResponse: Task execution response with details","operationId":"run_single_task_api_tasks_run__task_name__post","security":[{"OAuth2PasswordBearer":["tasks.run"]},{"HTTPBasic":[]}],"parameters":[{"name":"task_name","in":"path","required":true,"schema":{"type":"string","title":"Task Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskExecutionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/webrcade":{"get":{"tags":["feeds"],"summary":"Platforms Webrcade Feed","description":"Get webrcade feed endpoint\nhttps://docs.webrcade.com/feeds/format/\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n WebrcadeFeedSchema: Webrcade feed object schema","operationId":"platforms_webrcade_feed_api_feeds_webrcade_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebrcadeFeedSchema"}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/feeds/tinfoil":{"get":{"tags":["feeds"],"summary":"Tinfoil Index Feed","description":"Get tinfoil custom index feed endpoint\nhttps://blawar.github.io/tinfoil/custom_index/\n\nArgs:\n request (Request): Fastapi Request object\n slug (str, optional): Platform slug. Defaults to \"switch\".\n\nReturns:\n TinfoilFeedSchema: Tinfoil feed object schema","operationId":"tinfoil_index_feed_api_feeds_tinfoil_get","security":[{"OAuth2PasswordBearer":[]},{"HTTPBasic":[]}],"parameters":[{"name":"slug","in":"query","required":false,"schema":{"type":"string","default":"switch","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TinfoilFeedSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/pkgi/ps3/{content_type}":{"get":{"tags":["feeds"],"summary":"Pkgi Ps3 Feed","description":"Get PKGi PS3 feed endpoint\nhttps://github.com/bucanero/pkgi-ps3\n\nArgs:\n request (Request): Fastapi Request object\n content_type (str): Content type (game, dlc, demo, update, patch, mod, translation, prototype)\n\nReturns:\n Response: txt file with PKGi PS3 database format","operationId":"pkgi_ps3_feed_api_feeds_pkgi_ps3__content_type__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"content_type","in":"path","required":true,"schema":{"type":"string","description":"Content type","title":"Content Type"},"description":"Content type"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/pkgi/psvita/{content_type}":{"get":{"tags":["feeds"],"summary":"Pkgi Psvita Feed","description":"Get PKGi PS Vita feed endpoint\nhttps://github.com/mmozeiko/pkgi\n\nArgs:\n request (Request): Fastapi Request object\n content_type (str): Content type (game, dlc, demo, update, patch, mod, translation, prototype)\n\nReturns:\n Response: txt file with PKGi PS Vita database format","operationId":"pkgi_psvita_feed_api_feeds_pkgi_psvita__content_type__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"content_type","in":"path","required":true,"schema":{"type":"string","description":"Content type","title":"Content Type"},"description":"Content type"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/pkgi/psp/{content_type}":{"get":{"tags":["feeds"],"summary":"Pkgi Psp Feed","description":"Get PKGi PSP feed endpoint\nhttps://github.com/bucanero/pkgi-psp\n\nArgs:\n request (Request): Fastapi Request object\n content_type (str): Content type (game, dlc, demo, update, patch, mod, translation, prototype)\n\nReturns:\n Response: txt file with PKGi PSP database format","operationId":"pkgi_psp_feed_api_feeds_pkgi_psp__content_type__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"content_type","in":"path","required":true,"schema":{"type":"string","description":"Content type","title":"Content Type"},"description":"Content type"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/fpkgi/{platform_slug}":{"get":{"tags":["feeds"],"summary":"Fpkgi Feed","description":"https://github.com/ItsJokerZz/FPKGi\n\nArgs:\n request (Request): Fastapi Request object\n platform_slug (str): Platform slug (ps4, ps5)\n\nReturns:\n Response: JSON file in FPKGi format","operationId":"fpkgi_feed_api_feeds_fpkgi__platform_slug__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_slug","in":"path","required":true,"schema":{"type":"string","title":"Platform Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/kekatsu/{platform_slug}":{"get":{"tags":["feeds"],"summary":"Kekatsu Ds Feed","description":"Get Kekatsu DS feed endpoint\nhttps://github.com/cavv-dev/Kekatsu-DS\n\nArgs:\n request (Request): Fastapi Request object\n platform_slug (str): Platform slug (nds, nintendo-ds, ds, gba, etc.)\n\nReturns:\n Response: Text file with Kekatsu DS database format","operationId":"kekatsu_ds_feed_api_feeds_kekatsu__platform_slug__get","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_slug","in":"path","required":true,"schema":{"type":"string","title":"Platform Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feeds/pkgj/psp/games":{"get":{"tags":["feeds"],"summary":"Pkgj Psp Games Feed","operationId":"pkgj_psp_games_feed_api_feeds_pkgj_psp_games_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/feeds/pkgj/psp/dlc":{"get":{"tags":["feeds"],"summary":"Pkgj Psp Dlcs Feed","operationId":"pkgj_psp_dlcs_feed_api_feeds_pkgj_psp_dlc_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/feeds/pkgj/psvita/games":{"get":{"tags":["feeds"],"summary":"Pkgj Psv Games Feed","operationId":"pkgj_psv_games_feed_api_feeds_pkgj_psvita_games_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/feeds/pkgj/psvita/dlc":{"get":{"tags":["feeds"],"summary":"Pkgj Psv Dlcs Feed","operationId":"pkgj_psv_dlcs_feed_api_feeds_pkgj_psvita_dlc_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/feeds/pkgj/psx/games":{"get":{"tags":["feeds"],"summary":"Pkgj Psx Games Feed","operationId":"pkgj_psx_games_feed_api_feeds_pkgj_psx_games_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}]}},"/api/config":{"get":{"tags":["config"],"summary":"Get Config","description":"Get config endpoint\n\nReturns:\n ConfigResponse: RomM's configuration","operationId":"get_config_api_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigResponse"}}}}}}},"/api/config/system/platforms":{"post":{"tags":["config"],"summary":"Add Platform Binding","description":"Add platform binding to the configuration","operationId":"add_platform_binding_api_config_system_platforms_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}]}},"/api/config/system/platforms/{fs_slug}":{"delete":{"tags":["config"],"summary":"Delete Platform Binding","description":"Delete platform binding from the configuration","operationId":"delete_platform_binding_api_config_system_platforms__fs_slug__delete","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"fs_slug","in":"path","required":true,"schema":{"type":"string","title":"Fs Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/config/system/versions":{"post":{"tags":["config"],"summary":"Add Platform Version","description":"Add platform version to the configuration","operationId":"add_platform_version_api_config_system_versions_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}]}},"/api/config/system/versions/{fs_slug}":{"delete":{"tags":["config"],"summary":"Delete Platform Version","description":"Delete platform version from the configuration","operationId":"delete_platform_version_api_config_system_versions__fs_slug__delete","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"fs_slug","in":"path","required":true,"schema":{"type":"string","title":"Fs Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/config/exclude":{"post":{"tags":["config"],"summary":"Add Exclusion","description":"Add platform exclusion to the configuration","operationId":"add_exclusion_api_config_exclude_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}]}},"/api/config/exclude/{exclusion_type}/{exclusion_value}":{"delete":{"tags":["config"],"summary":"Delete Exclusion","description":"Delete platform binding from the configuration","operationId":"delete_exclusion_api_config_exclude__exclusion_type___exclusion_value__delete","security":[{"OAuth2PasswordBearer":["platforms.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"exclusion_type","in":"path","required":true,"schema":{"type":"string","title":"Exclusion Type"}},{"name":"exclusion_value","in":"path","required":true,"schema":{"type":"string","title":"Exclusion Value"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats":{"get":{"tags":["stats"],"summary":"Stats","description":"Endpoint to return the current RomM stats\n\nReturns:\n dict: Dictionary with all the stats","operationId":"stats_api_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsReturn"}}}}}}},"/api/raw/assets/{path}":{"head":{"tags":["raw"],"summary":"Head Raw Asset","operationId":"head_raw_asset_api_raw_assets__path__head","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["raw"],"summary":"Get Raw Asset","description":"Download a single asset file\n\nArgs:\n request (Request): Fastapi Request object\n path (str): Relative path to the asset file\n\nReturns:\n FileResponse: Returns a single asset file\n\nRaises:\n HTTPException: 404 if asset not found or access denied","operationId":"get_raw_asset_api_raw_assets__path__get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/screenshots":{"post":{"tags":["screenshots"],"summary":"Add Screenshot","operationId":"add_screenshot_api_screenshots_post","security":[{"OAuth2PasswordBearer":["assets.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"rom_id","in":"query","required":true,"schema":{"type":"integer","title":"Rom Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScreenshotSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/firmware":{"post":{"tags":["firmware"],"summary":"Add Firmware","description":"Upload firmware files endpoint\n\nArgs:\n request (Request): Fastapi Request object\n platform_slug (str): Slug of the platform where to upload the files\n files (list[UploadFile], optional): List of files to upload\n\nRaises:\n HTTPException\n\nReturns:\n AddFirmwareResponse: Standard message response","operationId":"add_firmware_api_firmware_post","security":[{"OAuth2PasswordBearer":["firmware.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_id","in":"query","required":true,"schema":{"type":"integer","title":"Platform Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_add_firmware_api_firmware_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddFirmwareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["firmware"],"summary":"Get Platform Firmware","description":"Get firmware endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[FirmwareSchema]: Firmware stored in the database","operationId":"get_platform_firmware_api_firmware_get","security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Platform Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FirmwareSchema"},"title":"Response Get Platform Firmware Api Firmware Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/firmware/identifiers":{"get":{"tags":["firmware"],"summary":"Get Firmware Identifiers","description":"Get firmware identifiers endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[int]: List of firmware IDs","operationId":"get_firmware_identifiers_api_firmware_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Get Firmware Identifiers Api Firmware Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}]}},"/api/firmware/{id}":{"get":{"tags":["firmware"],"summary":"Get Firmware","description":"Get firmware endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Firmware internal id\n\nReturns:\n FirmwareSchema: Firmware stored in the database","operationId":"get_firmware_api_firmware__id__get","security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmwareSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/firmware/{id}/content/{file_name}":{"head":{"tags":["firmware"],"summary":"Head Firmware Content","description":"Head firmware content endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Rom internal id\n file_name (str): Required due to a bug in emulatorjs\n\nReturns:\n FileResponse: Returns the response with headers","operationId":"head_firmware_content_api_firmware__id__content__file_name__head","security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","title":"File Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["firmware"],"summary":"Get Firmware Content","description":"Download firmware endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Rom internal id\n file_name (str): Required due to a bug in emulatorjs\n\nReturns:\n FileResponse: Returns the firmware file","operationId":"get_firmware_content_api_firmware__id__content__file_name__get","security":[{"OAuth2PasswordBearer":["firmware.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"file_name","in":"path","required":true,"schema":{"type":"string","title":"File Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/firmware/delete":{"post":{"tags":["firmware"],"summary":"Delete Firmware","description":"Delete firmware.","operationId":"delete_firmware_api_firmware_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_delete_firmware_api_firmware_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkOperationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":["firmware.write"]},{"HTTPBasic":[]}]}},"/api/collections":{"post":{"tags":["collections"],"summary":"Add Collection","description":"Create collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n CollectionSchema: Just created collection","operationId":"add_collection_api_collections_post","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"is_public","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}},{"name":"is_favorite","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Favorite"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_add_collection_api_collections_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["collections"],"summary":"Get Collections","description":"Get collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n updated_after: Filter collections updated after this datetime\n\nReturns:\n list[CollectionSchema]: List of collections","operationId":"get_collections_api_collections_get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"updated_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter collections updated after this datetime (ISO 8601 format with timezone information).","title":"Updated After"},"description":"Filter collections updated after this datetime (ISO 8601 format with timezone information)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CollectionSchema"},"title":"Response Get Collections Api Collections Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/smart":{"post":{"tags":["collections"],"summary":"Add Smart Collection","description":"Create smart collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n SmartCollectionSchema: Just created smart collection","operationId":"add_smart_collection_api_collections_smart_post","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"is_public","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartCollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["collections"],"summary":"Get Smart Collections","description":"Get smart collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n updated_after: Filter smart collections updated after this datetime\n\nReturns:\n list[SmartCollectionSchema]: List of smart collections","operationId":"get_smart_collections_api_collections_smart_get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"updated_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter smart collections updated after this datetime (ISO 8601 format with timezone information).","title":"Updated After"},"description":"Filter smart collections updated after this datetime (ISO 8601 format with timezone information)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SmartCollectionSchema"},"title":"Response Get Smart Collections Api Collections Smart Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/identifiers":{"get":{"tags":["collections"],"summary":"Get Collection Identifiers","description":"Get collections identifiers endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[int]: List of collection IDs","operationId":"get_collection_identifiers_api_collections_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Get Collection Identifiers Api Collections Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}]}},"/api/collections/virtual":{"get":{"tags":["collections"],"summary":"Get Virtual Collections","description":"Get virtual collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[VirtualCollectionSchema]: List of virtual collections","operationId":"get_virtual_collections_api_collections_virtual_get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"type","in":"query","required":true,"schema":{"type":"string","title":"Type"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VirtualCollectionSchema"},"title":"Response Get Virtual Collections Api Collections Virtual Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/virtual/identifiers":{"get":{"tags":["collections"],"summary":"Get Virtual Collection Identifiers","description":"Get virtual collections identifiers endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[str]: List of generated virtual collection IDs","operationId":"get_virtual_collection_identifiers_api_collections_virtual_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Response Get Virtual Collection Identifiers Api Collections Virtual Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}]}},"/api/collections/smart/identifiers":{"get":{"tags":["collections"],"summary":"Get Smart Collection Identifiers","description":"Get smart collections identifiers endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n list[int]: List of smart collection IDs","operationId":"get_smart_collection_identifiers_api_collections_smart_identifiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Response Get Smart Collection Identifiers Api Collections Smart Identifiers Get"}}}}},"security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}]}},"/api/collections/{id}":{"get":{"tags":["collections"],"summary":"Get Collection","description":"Get collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int, optional): Collection id. Defaults to None.\n\nReturns:\n CollectionSchema: Collection","operationId":"get_collection_api_collections__id__get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["collections"],"summary":"Update Collection","description":"Update collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n\nReturns:\n CollectionSchema: Updated collection","operationId":"update_collection_api_collections__id__put","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"remove_cover","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Remove Cover"}},{"name":"is_public","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_update_collection_api_collections__id__put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["collections"],"summary":"Delete Collection","description":"Delete a collection by ID.","operationId":"delete_collection_api_collections__id__delete","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Collection internal id.","title":"Id"},"description":"Collection internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/virtual/{id}":{"get":{"tags":["collections"],"summary":"Get Virtual Collection","description":"Get virtual collections endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (str): Virtual collection id\n\nReturns:\n VirtualCollectionSchema: Virtual collection","operationId":"get_virtual_collection_api_collections_virtual__id__get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualCollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/collections/smart/{id}":{"get":{"tags":["collections"],"summary":"Get Smart Collection","description":"Get smart collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Smart collection id\n\nReturns:\n SmartCollectionSchema: Smart collection","operationId":"get_smart_collection_api_collections_smart__id__get","security":[{"OAuth2PasswordBearer":["collections.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartCollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["collections"],"summary":"Update Smart Collection","description":"Update smart collection endpoint\n\nArgs:\n request (Request): Fastapi Request object\n id (int): Smart collection id\n\nReturns:\n SmartCollectionSchema: Updated smart collection","operationId":"update_smart_collection_api_collections_smart__id__put","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","title":"Id"}},{"name":"is_public","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartCollectionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["collections"],"summary":"Delete Smart Collection","description":"Delete a smart collection by ID.","operationId":"delete_smart_collection_api_collections_smart__id__delete","security":[{"OAuth2PasswordBearer":["collections.write"]},{"HTTPBasic":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1,"description":"Smart collection internal id.","title":"Id"},"description":"Smart collection internal id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/gamelist/export":{"post":{"tags":["gamelist"],"summary":"Export Gamelist","description":"Export platforms/ROMs to gamelist.xml format and write to platform directories","operationId":"export_gamelist_api_gamelist_export_post","security":[{"OAuth2PasswordBearer":["roms.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"platform_ids","in":"query","required":true,"schema":{"type":"array","items":{"type":"integer"},"description":"List of platform IDs to export","title":"Platform Ids"},"description":"List of platform IDs to export"},{"name":"local_export","in":"query","required":false,"schema":{"type":"boolean","description":"Use local paths instead of URLs","default":false,"title":"Local Export"},"description":"Use local paths instead of URLs"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/netplay/list":{"get":{"tags":["netplay"],"summary":"Get Rooms","operationId":"get_rooms_api_netplay_list_get","security":[{"OAuth2PasswordBearer":["assets.read"]},{"HTTPBasic":[]}],"parameters":[{"name":"game_id","in":"query","required":true,"schema":{"type":"string","title":"Game Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/RoomsResponse"},"title":"Response Get Rooms Api Netplay List Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AddFirmwareResponse":{"properties":{"uploaded":{"type":"integer","title":"Uploaded"},"firmware":{"items":{"$ref":"#/components/schemas/FirmwareSchema"},"type":"array","title":"Firmware"}},"type":"object","required":["uploaded","firmware"],"title":"AddFirmwareResponse"},"Body_add_collection_api_collections_post":{"properties":{"artwork":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Artwork"}},"type":"object","title":"Body_add_collection_api_collections_post"},"Body_add_firmware_api_firmware_post":{"properties":{"files":{"items":{"type":"string","format":"binary"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_add_firmware_api_firmware_post"},"Body_add_platform_api_platforms_post":{"properties":{"fs_slug":{"type":"string","title":"Fs Slug","description":"Platform slug."}},"type":"object","required":["fs_slug"],"title":"Body_add_platform_api_platforms_post"},"Body_add_user_api_users_post":{"properties":{"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"role":{"type":"string","title":"Role"}},"type":"object","required":["username","email","password","role"],"title":"Body_add_user_api_users_post"},"Body_confirm_download_api_saves__id__downloaded_post":{"properties":{"device_id":{"type":"string","title":"Device Id"}},"type":"object","required":["device_id"],"title":"Body_confirm_download_api_saves__id__downloaded_post"},"Body_create_user_from_invite_api_users_register_post":{"properties":{"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"token":{"type":"string","title":"Token"}},"type":"object","required":["username","email","password","token"],"title":"Body_create_user_from_invite_api_users_register_post"},"Body_delete_firmware_api_firmware_delete_post":{"properties":{"firmware":{"items":{"type":"integer"},"type":"array","title":"Firmware","description":"List of firmware ids to delete from database."},"delete_from_fs":{"items":{"type":"integer"},"type":"array","title":"Delete From Fs","description":"List of firmware ids to delete from filesystem."}},"type":"object","required":["firmware"],"title":"Body_delete_firmware_api_firmware_delete_post"},"Body_delete_roms_api_roms_delete_post":{"properties":{"roms":{"items":{"type":"integer"},"type":"array","title":"Roms","description":"List of rom ids to delete from database."},"delete_from_fs":{"items":{"type":"integer"},"type":"array","title":"Delete From Fs","description":"List of rom ids to delete from filesystem."}},"type":"object","required":["roms"],"title":"Body_delete_roms_api_roms_delete_post"},"Body_delete_saves_api_saves_delete_post":{"properties":{"saves":{"items":{"type":"integer"},"type":"array","title":"Saves","description":"List of save ids to delete from database."}},"type":"object","required":["saves"],"title":"Body_delete_saves_api_saves_delete_post"},"Body_delete_states_api_states_delete_post":{"properties":{"states":{"items":{"type":"integer"},"type":"array","title":"States","description":"List of states ids to delete from database."}},"type":"object","required":["states"],"title":"Body_delete_states_api_states_delete_post"},"Body_refresh_retro_achievements_api_users__id__ra_refresh_post":{"properties":{"incremental":{"type":"boolean","title":"Incremental","description":"Whether to only retrieve RetroAchievements progression incrementally.","default":false}},"type":"object","title":"Body_refresh_retro_achievements_api_users__id__ra_refresh_post"},"Body_request_password_reset_api_forgot_password_post":{"properties":{"username":{"type":"string","title":"Username"}},"type":"object","required":["username"],"title":"Body_request_password_reset_api_forgot_password_post"},"Body_reset_password_api_reset_password_post":{"properties":{"token":{"type":"string","title":"Token"},"new_password":{"type":"string","title":"New Password"}},"type":"object","required":["token","new_password"],"title":"Body_reset_password_api_reset_password_post"},"Body_token_api_token_post":{"properties":{"grant_type":{"type":"string","title":"Grant Type","default":"password"},"scope":{"type":"string","title":"Scope","default":""},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Secret"},"refresh_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Token"}},"type":"object","title":"Body_token_api_token_post"},"Body_track_save_api_saves__id__track_post":{"properties":{"device_id":{"type":"string","title":"Device Id"}},"type":"object","required":["device_id"],"title":"Body_track_save_api_saves__id__track_post"},"Body_untrack_save_api_saves__id__untrack_post":{"properties":{"device_id":{"type":"string","title":"Device Id"}},"type":"object","required":["device_id"],"title":"Body_untrack_save_api_saves__id__untrack_post"},"Body_update_collection_api_collections__id__put":{"properties":{"artwork":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Artwork"}},"type":"object","title":"Body_update_collection_api_collections__id__put"},"Body_update_platform_api_platforms__id__put":{"properties":{"aspect_ratio":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aspect Ratio","description":"Cover aspect ratio."},"custom_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Name","description":"Custom platform name."}},"type":"object","title":"Body_update_platform_api_platforms__id__put"},"Body_update_rom_api_roms__id__put":{"properties":{"artwork":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Artwork","description":"Custom artwork to set as cover."}},"type":"object","title":"Body_update_rom_api_roms__id__put"},"Body_update_rom_user_api_roms__id__props_put":{"properties":{"update_last_played":{"type":"boolean","title":"Update Last Played","description":"Whether to update the last played date.","default":false},"remove_last_played":{"type":"boolean","title":"Remove Last Played","description":"Whether to remove the last played date.","default":false}},"type":"object","title":"Body_update_rom_user_api_roms__id__props_put"},"BulkOperationResponse":{"properties":{"successful_items":{"type":"integer","title":"Successful Items"},"failed_items":{"type":"integer","title":"Failed Items"},"errors":{"items":{"type":"string"},"type":"array","title":"Errors"}},"type":"object","required":["successful_items","failed_items","errors"],"title":"BulkOperationResponse"},"CleanupStats":{"properties":{"platforms_in_db":{"type":"integer","title":"Platforms In Db"},"roms_in_db":{"type":"integer","title":"Roms In Db"},"platforms_in_fs":{"type":"integer","title":"Platforms In Fs"},"roms_in_fs":{"type":"integer","title":"Roms In Fs"},"removed_fs_platforms":{"type":"integer","title":"Removed Fs Platforms"},"removed_fs_roms":{"type":"integer","title":"Removed Fs Roms"}},"type":"object","required":["platforms_in_db","roms_in_db","platforms_in_fs","roms_in_fs","removed_fs_platforms","removed_fs_roms"],"title":"CleanupStats"},"CleanupTaskMeta":{"properties":{"cleanup_stats":{"anyOf":[{"$ref":"#/components/schemas/CleanupStats"},{"type":"null"}]}},"type":"object","required":["cleanup_stats"],"title":"CleanupTaskMeta"},"CleanupTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"cleanup","title":"Task Type"},"meta":{"$ref":"#/components/schemas/CleanupTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"CleanupTaskStatusResponse"},"CollectionSchema":{"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"rom_ids":{"items":{"type":"integer"},"type":"array","uniqueItems":true,"title":"Rom Ids"},"rom_count":{"type":"integer","title":"Rom Count"},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"path_covers_small":{"items":{"type":"string"},"type":"array","title":"Path Covers Small"},"path_covers_large":{"items":{"type":"string"},"type":"array","title":"Path Covers Large"},"is_public":{"type":"boolean","title":"Is Public","default":false},"is_favorite":{"type":"boolean","title":"Is Favorite","default":false},"is_virtual":{"type":"boolean","title":"Is Virtual","default":false},"is_smart":{"type":"boolean","title":"Is Smart","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"id":{"type":"integer","title":"Id"},"url_cover":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Cover"},"user_id":{"type":"integer","title":"User Id"},"owner_username":{"type":"string","title":"Owner Username"}},"type":"object","required":["name","description","rom_ids","rom_count","path_cover_small","path_cover_large","path_covers_small","path_covers_large","created_at","updated_at","id","url_cover","user_id","owner_username"],"title":"CollectionSchema"},"ConfigResponse":{"properties":{"CONFIG_FILE_MOUNTED":{"type":"boolean","title":"Config File Mounted"},"CONFIG_FILE_WRITABLE":{"type":"boolean","title":"Config File Writable"},"EXCLUDED_PLATFORMS":{"items":{"type":"string"},"type":"array","title":"Excluded Platforms"},"EXCLUDED_SINGLE_EXT":{"items":{"type":"string"},"type":"array","title":"Excluded Single Ext"},"EXCLUDED_SINGLE_FILES":{"items":{"type":"string"},"type":"array","title":"Excluded Single Files"},"EXCLUDED_MULTI_FILES":{"items":{"type":"string"},"type":"array","title":"Excluded Multi Files"},"EXCLUDED_MULTI_PARTS_EXT":{"items":{"type":"string"},"type":"array","title":"Excluded Multi Parts Ext"},"EXCLUDED_MULTI_PARTS_FILES":{"items":{"type":"string"},"type":"array","title":"Excluded Multi Parts Files"},"PLATFORMS_BINDING":{"additionalProperties":{"type":"string"},"type":"object","title":"Platforms Binding"},"PLATFORMS_VERSIONS":{"additionalProperties":{"type":"string"},"type":"object","title":"Platforms Versions"},"SKIP_HASH_CALCULATION":{"type":"boolean","title":"Skip Hash Calculation"},"EJS_DEBUG":{"type":"boolean","title":"Ejs Debug"},"EJS_CACHE_LIMIT":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ejs Cache Limit"},"EJS_DISABLE_AUTO_UNLOAD":{"type":"boolean","title":"Ejs Disable Auto Unload"},"EJS_DISABLE_BATCH_BOOTUP":{"type":"boolean","title":"Ejs Disable Batch Bootup"},"EJS_NETPLAY_ENABLED":{"type":"boolean","title":"Ejs Netplay Enabled"},"EJS_NETPLAY_ICE_SERVERS":{"items":{"$ref":"#/components/schemas/NetplayICEServer"},"type":"array","title":"Ejs Netplay Ice Servers"},"EJS_SETTINGS":{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object","title":"Ejs Settings"},"EJS_CONTROLS":{"additionalProperties":{"$ref":"#/components/schemas/EjsControls"},"type":"object","title":"Ejs Controls"},"SCAN_METADATA_PRIORITY":{"items":{"type":"string"},"type":"array","title":"Scan Metadata Priority"},"SCAN_ARTWORK_PRIORITY":{"items":{"type":"string"},"type":"array","title":"Scan Artwork Priority"},"SCAN_REGION_PRIORITY":{"items":{"type":"string"},"type":"array","title":"Scan Region Priority"},"SCAN_LANGUAGE_PRIORITY":{"items":{"type":"string"},"type":"array","title":"Scan Language Priority"},"SCAN_MEDIA":{"items":{"type":"string"},"type":"array","title":"Scan Media"}},"type":"object","required":["CONFIG_FILE_MOUNTED","CONFIG_FILE_WRITABLE","EXCLUDED_PLATFORMS","EXCLUDED_SINGLE_EXT","EXCLUDED_SINGLE_FILES","EXCLUDED_MULTI_FILES","EXCLUDED_MULTI_PARTS_EXT","EXCLUDED_MULTI_PARTS_FILES","PLATFORMS_BINDING","PLATFORMS_VERSIONS","SKIP_HASH_CALCULATION","EJS_DEBUG","EJS_CACHE_LIMIT","EJS_DISABLE_AUTO_UNLOAD","EJS_DISABLE_BATCH_BOOTUP","EJS_NETPLAY_ENABLED","EJS_NETPLAY_ICE_SERVERS","EJS_SETTINGS","EJS_CONTROLS","SCAN_METADATA_PRIORITY","SCAN_ARTWORK_PRIORITY","SCAN_REGION_PRIORITY","SCAN_LANGUAGE_PRIORITY","SCAN_MEDIA"],"title":"ConfigResponse"},"ConversionStats":{"properties":{"processed":{"type":"integer","title":"Processed"},"errors":{"type":"integer","title":"Errors"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["processed","errors","total"],"title":"ConversionStats"},"ConversionTaskMeta":{"properties":{"conversion_stats":{"anyOf":[{"$ref":"#/components/schemas/ConversionStats"},{"type":"null"}]}},"type":"object","required":["conversion_stats"],"title":"ConversionTaskMeta"},"ConversionTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"conversion","title":"Task Type"},"meta":{"$ref":"#/components/schemas/ConversionTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"ConversionTaskStatusResponse"},"CustomLimitOffsetPage_SimpleRomSchema_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SimpleRomSchema"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"limit":{"type":"integer","minimum":1.0,"title":"Limit"},"offset":{"type":"integer","minimum":0.0,"title":"Offset"},"char_index":{"additionalProperties":{"type":"integer"},"type":"object","title":"Char Index"},"rom_id_index":{"items":{"type":"integer"},"type":"array","title":"Rom Id Index"},"filter_values":{"$ref":"#/components/schemas/RomFiltersDict"}},"type":"object","required":["items","total","limit","offset","char_index","rom_id_index","filter_values"],"title":"CustomLimitOffsetPage[SimpleRomSchema]"},"DetailedRomSchema":{"properties":{"id":{"type":"integer","title":"Id"},"igdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Igdb Id"},"sgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sgdb Id"},"moby_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Moby Id"},"ss_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ss Id"},"ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ra Id"},"launchbox_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Launchbox Id"},"hasheous_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hasheous Id"},"tgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tgdb Id"},"flashpoint_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flashpoint Id"},"hltb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hltb Id"},"gamelist_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gamelist Id"},"platform_id":{"type":"integer","title":"Platform Id"},"platform_slug":{"type":"string","title":"Platform Slug"},"platform_fs_slug":{"type":"string","title":"Platform Fs Slug"},"platform_custom_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform Custom Name"},"platform_display_name":{"type":"string","title":"Platform Display Name"},"fs_name":{"type":"string","title":"Fs Name"},"fs_name_no_tags":{"type":"string","title":"Fs Name No Tags"},"fs_name_no_ext":{"type":"string","title":"Fs Name No Ext"},"fs_extension":{"type":"string","title":"Fs Extension"},"fs_path":{"type":"string","title":"Fs Path"},"fs_size_bytes":{"type":"integer","title":"Fs Size Bytes"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"alternative_names":{"items":{"type":"string"},"type":"array","title":"Alternative Names"},"youtube_video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Video Id"},"metadatum":{"$ref":"#/components/schemas/RomMetadataSchema"},"igdb_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomIGDBMetadata"},{"type":"null"}]},"moby_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomMobyMetadata"},{"type":"null"}]},"ss_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomSSMetadata"},{"type":"null"}]},"launchbox_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomLaunchboxMetadata"},{"type":"null"}]},"hasheous_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomHasheousMetadata"},{"type":"null"}]},"flashpoint_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomFlashpointMetadata"},{"type":"null"}]},"hltb_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomHLTBMetadata"},{"type":"null"}]},"gamelist_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomGamelistMetadata"},{"type":"null"}]},"manual_metadata":{"anyOf":[{"$ref":"#/components/schemas/ManualMetadata"},{"type":"null"}]},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"url_cover":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Cover"},"has_manual":{"type":"boolean","title":"Has Manual"},"path_manual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Manual"},"url_manual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Manual"},"is_identifying":{"type":"boolean","title":"Is Identifying","default":false},"is_unidentified":{"type":"boolean","title":"Is Unidentified"},"is_identified":{"type":"boolean","title":"Is Identified"},"revision":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revision"},"regions":{"items":{"type":"string"},"type":"array","title":"Regions"},"languages":{"items":{"type":"string"},"type":"array","title":"Languages"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"crc_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crc Hash"},"md5_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5 Hash"},"sha1_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sha1 Hash"},"ra_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ra Hash"},"has_simple_single_file":{"type":"boolean","title":"Has Simple Single File"},"has_nested_single_file":{"type":"boolean","title":"Has Nested Single File"},"has_multiple_files":{"type":"boolean","title":"Has Multiple Files"},"files":{"items":{"$ref":"#/components/schemas/RomFileSchema"},"type":"array","title":"Files"},"full_path":{"type":"string","title":"Full Path"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"has_notes":{"type":"boolean","title":"Has Notes"},"siblings":{"items":{"$ref":"#/components/schemas/SiblingRomSchema"},"type":"array","title":"Siblings"},"rom_user":{"$ref":"#/components/schemas/RomUserSchema"},"merged_screenshots":{"items":{"type":"string"},"type":"array","title":"Merged Screenshots"},"merged_ra_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomRAMetadata"},{"type":"null"}]},"user_saves":{"items":{"$ref":"#/components/schemas/SaveSchema"},"type":"array","title":"User Saves"},"user_states":{"items":{"$ref":"#/components/schemas/StateSchema"},"type":"array","title":"User States"},"user_screenshots":{"items":{"$ref":"#/components/schemas/ScreenshotSchema"},"type":"array","title":"User Screenshots"},"user_collections":{"items":{"$ref":"#/components/schemas/UserCollectionSchema"},"type":"array","title":"User Collections"},"all_user_notes":{"items":{"$ref":"#/components/schemas/UserNoteSchema"},"type":"array","title":"All User Notes"}},"type":"object","required":["id","igdb_id","sgdb_id","moby_id","ss_id","ra_id","launchbox_id","hasheous_id","tgdb_id","flashpoint_id","hltb_id","gamelist_id","platform_id","platform_slug","platform_fs_slug","platform_custom_name","platform_display_name","fs_name","fs_name_no_tags","fs_name_no_ext","fs_extension","fs_path","fs_size_bytes","name","slug","summary","alternative_names","youtube_video_id","metadatum","igdb_metadata","moby_metadata","ss_metadata","launchbox_metadata","hasheous_metadata","flashpoint_metadata","hltb_metadata","gamelist_metadata","manual_metadata","path_cover_small","path_cover_large","url_cover","has_manual","path_manual","url_manual","is_unidentified","is_identified","revision","regions","languages","tags","crc_hash","md5_hash","sha1_hash","ra_hash","has_simple_single_file","has_nested_single_file","has_multiple_files","files","full_path","created_at","updated_at","missing_from_fs","has_notes","siblings","rom_user","merged_screenshots","merged_ra_metadata","user_saves","user_states","user_screenshots","user_collections","all_user_notes"],"title":"DetailedRomSchema"},"DeviceCreatePayload":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"client":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client"},"client_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Version"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"},"mac_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mac Address"},"hostname":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hostname"},"allow_existing":{"type":"boolean","title":"Allow Existing","default":true},"allow_duplicate":{"type":"boolean","title":"Allow Duplicate","default":false},"reset_syncs":{"type":"boolean","title":"Reset Syncs","default":false}},"type":"object","title":"DeviceCreatePayload"},"DeviceCreateResponse":{"properties":{"device_id":{"type":"string","title":"Device Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["device_id","name","created_at"],"title":"DeviceCreateResponse"},"DeviceSchema":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"integer","title":"User Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"client":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client"},"client_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Version"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"},"mac_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mac Address"},"hostname":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hostname"},"sync_mode":{"$ref":"#/components/schemas/SyncMode"},"sync_enabled":{"type":"boolean","title":"Sync Enabled"},"last_seen":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Seen"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","user_id","name","platform","client","client_version","ip_address","mac_address","hostname","sync_mode","sync_enabled","last_seen","created_at","updated_at"],"title":"DeviceSchema"},"DeviceSyncSchema":{"properties":{"device_id":{"type":"string","title":"Device Id"},"device_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Name"},"last_synced_at":{"type":"string","format":"date-time","title":"Last Synced At"},"is_untracked":{"type":"boolean","title":"Is Untracked"},"is_current":{"type":"boolean","title":"Is Current"}},"type":"object","required":["device_id","device_name","last_synced_at","is_untracked","is_current"],"title":"DeviceSyncSchema"},"DeviceUpdatePayload":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"client":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client"},"client_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Version"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"},"mac_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mac Address"},"hostname":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hostname"},"sync_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sync Enabled"}},"type":"object","title":"DeviceUpdatePayload"},"EarnedAchievement":{"properties":{"id":{"type":"string","title":"Id"},"date":{"type":"string","title":"Date"},"date_hardcore":{"type":"string","title":"Date Hardcore"}},"type":"object","required":["id","date"],"title":"EarnedAchievement"},"EjsControls":{"properties":{"_0":{"additionalProperties":{"$ref":"#/components/schemas/EjsControlsButton"},"type":"object","title":"0"},"_1":{"additionalProperties":{"$ref":"#/components/schemas/EjsControlsButton"},"type":"object","title":"1"},"_2":{"additionalProperties":{"$ref":"#/components/schemas/EjsControlsButton"},"type":"object","title":"2"},"_3":{"additionalProperties":{"$ref":"#/components/schemas/EjsControlsButton"},"type":"object","title":"3"}},"type":"object","required":["_0","_1","_2","_3"],"title":"EjsControls"},"EjsControlsButton":{"properties":{"value":{"type":"string","title":"Value"},"value2":{"type":"string","title":"Value2"}},"type":"object","title":"EjsControlsButton"},"EmulationDict":{"properties":{"DISABLE_EMULATOR_JS":{"type":"boolean","title":"Disable Emulator Js"},"DISABLE_RUFFLE_RS":{"type":"boolean","title":"Disable Ruffle Rs"}},"type":"object","required":["DISABLE_EMULATOR_JS","DISABLE_RUFFLE_RS"],"title":"EmulationDict"},"FilesystemDict":{"properties":{"FS_PLATFORMS":{"items":{"type":"string"},"type":"array","title":"Fs Platforms"}},"type":"object","required":["FS_PLATFORMS"],"title":"FilesystemDict"},"FirmwareSchema":{"properties":{"id":{"type":"integer","title":"Id"},"file_name":{"type":"string","title":"File Name"},"file_name_no_tags":{"type":"string","title":"File Name No Tags"},"file_name_no_ext":{"type":"string","title":"File Name No Ext"},"file_extension":{"type":"string","title":"File Extension"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"is_verified":{"type":"boolean","title":"Is Verified"},"crc_hash":{"type":"string","title":"Crc Hash"},"md5_hash":{"type":"string","title":"Md5 Hash"},"sha1_hash":{"type":"string","title":"Sha1 Hash"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","file_name","file_name_no_tags","file_name_no_ext","file_extension","file_path","file_size_bytes","full_path","is_verified","crc_hash","md5_hash","sha1_hash","missing_from_fs","created_at","updated_at"],"title":"FirmwareSchema"},"FrontendDict":{"properties":{"UPLOAD_TIMEOUT":{"type":"integer","title":"Upload Timeout"},"DISABLE_USERPASS_LOGIN":{"type":"boolean","title":"Disable Userpass Login"},"YOUTUBE_BASE_URL":{"type":"string","title":"Youtube Base Url"}},"type":"object","required":["UPLOAD_TIMEOUT","DISABLE_USERPASS_LOGIN","YOUTUBE_BASE_URL"],"title":"FrontendDict"},"GenericTaskMeta":{"properties":{},"type":"object","title":"GenericTaskMeta"},"GenericTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"generic","title":"Task Type"},"meta":{"$ref":"#/components/schemas/GenericTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"GenericTaskStatusResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HeartbeatResponse":{"properties":{"SYSTEM":{"$ref":"#/components/schemas/SystemDict"},"METADATA_SOURCES":{"$ref":"#/components/schemas/MetadataSourcesDict"},"FILESYSTEM":{"$ref":"#/components/schemas/FilesystemDict"},"EMULATION":{"$ref":"#/components/schemas/EmulationDict"},"FRONTEND":{"$ref":"#/components/schemas/FrontendDict"},"OIDC":{"$ref":"#/components/schemas/OIDCDict"},"TASKS":{"$ref":"#/components/schemas/TasksDict"}},"type":"object","required":["SYSTEM","METADATA_SOURCES","FILESYSTEM","EMULATION","FRONTEND","OIDC","TASKS"],"title":"HeartbeatResponse"},"IGDBAgeRating":{"properties":{"rating":{"type":"string","title":"Rating"},"category":{"type":"string","title":"Category"},"rating_cover_url":{"type":"string","title":"Rating Cover Url"}},"type":"object","required":["rating","category","rating_cover_url"],"title":"IGDBAgeRating"},"IGDBMetadataMultiplayerMode":{"properties":{"campaigncoop":{"type":"boolean","title":"Campaigncoop"},"dropin":{"type":"boolean","title":"Dropin"},"lancoop":{"type":"boolean","title":"Lancoop"},"offlinecoop":{"type":"boolean","title":"Offlinecoop"},"offlinecoopmax":{"type":"integer","title":"Offlinecoopmax"},"offlinemax":{"type":"integer","title":"Offlinemax"},"onlinecoop":{"type":"integer","title":"Onlinecoop"},"onlinecoopmax":{"type":"integer","title":"Onlinecoopmax"},"onlinemax":{"type":"integer","title":"Onlinemax"},"splitscreen":{"type":"boolean","title":"Splitscreen"},"splitscreenonline":{"type":"boolean","title":"Splitscreenonline"},"platform":{"$ref":"#/components/schemas/IGDBMetadataPlatform"}},"type":"object","required":["campaigncoop","dropin","lancoop","offlinecoop","offlinecoopmax","offlinemax","onlinecoop","onlinecoopmax","onlinemax","splitscreen","splitscreenonline","platform"],"title":"IGDBMetadataMultiplayerMode"},"IGDBMetadataPlatform":{"properties":{"igdb_id":{"type":"integer","title":"Igdb Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["igdb_id","name"],"title":"IGDBMetadataPlatform"},"IGDBRelatedGame":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"type":{"type":"string","title":"Type"},"cover_url":{"type":"string","title":"Cover Url"}},"type":"object","required":["id","name","slug","type","cover_url"],"title":"IGDBRelatedGame"},"InviteLinkSchema":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"InviteLinkSchema"},"JobStatus":{"type":"string","enum":["queued","finished","failed","started","deferred","scheduled","stopped","canceled"],"title":"JobStatus","description":"The Status of Job within its lifecycle at any given time."},"LaunchboxImage":{"properties":{"url":{"type":"string","title":"Url"},"type":{"type":"string","title":"Type"},"region":{"type":"string","title":"Region"}},"type":"object","required":["url"],"title":"LaunchboxImage"},"ManualMetadata":{"properties":{"genres":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Genres"},"franchises":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Franchises"},"companies":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Companies"},"game_modes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Game Modes"},"age_ratings":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Age Ratings"},"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"youtube_video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Video Id"}},"type":"object","title":"ManualMetadata"},"MetadataSourcesDict":{"properties":{"ANY_SOURCE_ENABLED":{"type":"boolean","title":"Any Source Enabled"},"IGDB_API_ENABLED":{"type":"boolean","title":"Igdb Api Enabled"},"SS_API_ENABLED":{"type":"boolean","title":"Ss Api Enabled"},"MOBY_API_ENABLED":{"type":"boolean","title":"Moby Api Enabled"},"STEAMGRIDDB_API_ENABLED":{"type":"boolean","title":"Steamgriddb Api Enabled"},"RA_API_ENABLED":{"type":"boolean","title":"Ra Api Enabled"},"LAUNCHBOX_API_ENABLED":{"type":"boolean","title":"Launchbox Api Enabled"},"HASHEOUS_API_ENABLED":{"type":"boolean","title":"Hasheous Api Enabled"},"PLAYMATCH_API_ENABLED":{"type":"boolean","title":"Playmatch Api Enabled"},"TGDB_API_ENABLED":{"type":"boolean","title":"Tgdb Api Enabled"},"FLASHPOINT_API_ENABLED":{"type":"boolean","title":"Flashpoint Api Enabled"},"HLTB_API_ENABLED":{"type":"boolean","title":"Hltb Api Enabled"}},"type":"object","required":["ANY_SOURCE_ENABLED","IGDB_API_ENABLED","SS_API_ENABLED","MOBY_API_ENABLED","STEAMGRIDDB_API_ENABLED","RA_API_ENABLED","LAUNCHBOX_API_ENABLED","HASHEOUS_API_ENABLED","PLAYMATCH_API_ENABLED","TGDB_API_ENABLED","FLASHPOINT_API_ENABLED","HLTB_API_ENABLED"],"title":"MetadataSourcesDict"},"MobyMetadataPlatform":{"properties":{"moby_id":{"type":"integer","title":"Moby Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["moby_id","name"],"title":"MobyMetadataPlatform"},"NetplayICEServer":{"properties":{"urls":{"type":"string","title":"Urls"},"username":{"type":"string","title":"Username"},"credential":{"type":"string","title":"Credential"}},"type":"object","required":["urls"],"title":"NetplayICEServer"},"OIDCDict":{"properties":{"ENABLED":{"type":"boolean","title":"Enabled"},"AUTOLOGIN":{"type":"boolean","title":"Autologin"},"PROVIDER":{"type":"string","title":"Provider"}},"type":"object","required":["ENABLED","AUTOLOGIN","PROVIDER"],"title":"OIDCDict"},"PlatformSchema":{"properties":{"id":{"type":"integer","title":"Id"},"slug":{"type":"string","title":"Slug"},"fs_slug":{"type":"string","title":"Fs Slug"},"rom_count":{"type":"integer","title":"Rom Count"},"name":{"type":"string","title":"Name"},"igdb_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Igdb Slug"},"moby_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Moby Slug"},"hltb_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hltb Slug"},"custom_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Name"},"igdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Igdb Id"},"sgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sgdb Id"},"moby_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Moby Id"},"launchbox_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Launchbox Id"},"ss_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ss Id"},"ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ra Id"},"hasheous_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hasheous Id"},"tgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tgdb Id"},"flashpoint_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Flashpoint Id"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"generation":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Generation"},"family_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Family Name"},"family_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Family Slug"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"url_logo":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Logo"},"firmware":{"items":{"$ref":"#/components/schemas/FirmwareSchema"},"type":"array","title":"Firmware"},"aspect_ratio":{"type":"string","title":"Aspect Ratio","default":"2 / 3"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"fs_size_bytes":{"type":"integer","title":"Fs Size Bytes"},"is_unidentified":{"type":"boolean","title":"Is Unidentified"},"is_identified":{"type":"boolean","title":"Is Identified"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"display_name":{"type":"string","title":"Display Name","readOnly":true}},"type":"object","required":["id","slug","fs_slug","rom_count","name","igdb_slug","moby_slug","hltb_slug","created_at","updated_at","fs_size_bytes","is_unidentified","is_identified","missing_from_fs","display_name"],"title":"PlatformSchema"},"RAGameRomAchievement":{"properties":{"ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ra Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"points":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Points"},"num_awarded":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Num Awarded"},"num_awarded_hardcore":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Num Awarded Hardcore"},"badge_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Id"},"badge_url_lock":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Url Lock"},"badge_path_lock":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Path Lock"},"badge_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Url"},"badge_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge Path"},"display_order":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Display Order"},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type"}},"type":"object","required":["ra_id","title","description","points","num_awarded","num_awarded_hardcore","badge_id","badge_url_lock","badge_path_lock","badge_url","badge_path","display_order","type"],"title":"RAGameRomAchievement"},"RAProgression":{"properties":{"total":{"type":"integer","title":"Total"},"results":{"items":{"$ref":"#/components/schemas/RAUserGameProgression"},"type":"array","title":"Results"}},"type":"object","title":"RAProgression"},"RAUserGameProgression":{"properties":{"rom_ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rom Ra Id"},"max_possible":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Possible"},"num_awarded":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Num Awarded"},"num_awarded_hardcore":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Num Awarded Hardcore"},"most_recent_awarded_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Most Recent Awarded Date"},"earned_achievements":{"items":{"$ref":"#/components/schemas/EarnedAchievement"},"type":"array","title":"Earned Achievements"}},"type":"object","required":["rom_ra_id","max_possible","num_awarded","num_awarded_hardcore","earned_achievements"],"title":"RAUserGameProgression"},"Role":{"type":"string","enum":["viewer","editor","admin"],"title":"Role"},"RomFileCategory":{"type":"string","enum":["game","dlc","hack","manual","patch","update","mod","demo","translation","prototype","cheat"],"title":"RomFileCategory"},"RomFileSchema":{"properties":{"id":{"type":"integer","title":"Id"},"rom_id":{"type":"integer","title":"Rom Id"},"file_name":{"type":"string","title":"File Name"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"last_modified":{"type":"string","format":"date-time","title":"Last Modified"},"crc_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crc Hash"},"md5_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5 Hash"},"sha1_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sha1 Hash"},"ra_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ra Hash"},"category":{"anyOf":[{"$ref":"#/components/schemas/RomFileCategory"},{"type":"null"}]}},"type":"object","required":["id","rom_id","file_name","file_path","file_size_bytes","full_path","created_at","updated_at","last_modified","crc_hash","md5_hash","sha1_hash","ra_hash","category"],"title":"RomFileSchema"},"RomFiltersDict":{"properties":{"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"collections":{"items":{"type":"string"},"type":"array","title":"Collections"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"age_ratings":{"items":{"type":"string"},"type":"array","title":"Age Ratings"},"player_counts":{"items":{"type":"string"},"type":"array","title":"Player Counts"},"regions":{"items":{"type":"string"},"type":"array","title":"Regions"},"languages":{"items":{"type":"string"},"type":"array","title":"Languages"},"platforms":{"items":{"type":"integer"},"type":"array","title":"Platforms"}},"type":"object","required":["genres","franchises","collections","companies","game_modes","age_ratings","player_counts","regions","languages","platforms"],"title":"RomFiltersDict"},"RomFlashpointMetadata":{"properties":{"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"first_release_date":{"type":"string","title":"First Release Date"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"RomFlashpointMetadata"},"RomGamelistMetadata":{"properties":{"box2d_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Url"},"box2d_back_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Back Url"},"box3d_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box3D Url"},"fanart_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fanart Url"},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url"},"manual_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manual Url"},"marquee_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marquee Url"},"miximage_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Miximage Url"},"physical_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical Url"},"screenshot_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screenshot Url"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url"},"title_screen_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title Screen Url"},"video_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Url"},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating"},"first_release_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Release Date"},"companies":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Companies"},"franchises":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Franchises"},"genres":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Genres"},"player_count":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Player Count"},"md5_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5 Hash"},"box3d_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box3D Path"},"miximage_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Miximage Path"},"physical_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical Path"},"marquee_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marquee Path"},"video_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Path"}},"type":"object","title":"RomGamelistMetadata"},"RomHLTBMetadata":{"properties":{"main_story":{"type":"integer","title":"Main Story"},"main_story_count":{"type":"integer","title":"Main Story Count"},"main_plus_extra":{"type":"integer","title":"Main Plus Extra"},"main_plus_extra_count":{"type":"integer","title":"Main Plus Extra Count"},"completionist":{"type":"integer","title":"Completionist"},"completionist_count":{"type":"integer","title":"Completionist Count"},"all_styles":{"type":"integer","title":"All Styles"},"all_styles_count":{"type":"integer","title":"All Styles Count"},"release_year":{"type":"integer","title":"Release Year"},"review_score":{"type":"integer","title":"Review Score"},"review_count":{"type":"integer","title":"Review Count"},"popularity":{"type":"integer","title":"Popularity"},"completions":{"type":"integer","title":"Completions"}},"type":"object","title":"RomHLTBMetadata"},"RomHasheousMetadata":{"properties":{"tosec_match":{"type":"boolean","title":"Tosec Match"},"mame_arcade_match":{"type":"boolean","title":"Mame Arcade Match"},"mame_mess_match":{"type":"boolean","title":"Mame Mess Match"},"nointro_match":{"type":"boolean","title":"Nointro Match"},"redump_match":{"type":"boolean","title":"Redump Match"},"whdload_match":{"type":"boolean","title":"Whdload Match"},"ra_match":{"type":"boolean","title":"Ra Match"},"fbneo_match":{"type":"boolean","title":"Fbneo Match"},"puredos_match":{"type":"boolean","title":"Puredos Match"}},"type":"object","title":"RomHasheousMetadata"},"RomIGDBMetadata":{"properties":{"total_rating":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Total Rating"},"aggregated_rating":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aggregated Rating"},"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"youtube_video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Video Id"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"alternative_names":{"items":{"type":"string"},"type":"array","title":"Alternative Names"},"collections":{"items":{"type":"string"},"type":"array","title":"Collections"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"age_ratings":{"items":{"$ref":"#/components/schemas/IGDBAgeRating"},"type":"array","title":"Age Ratings"},"platforms":{"items":{"$ref":"#/components/schemas/IGDBMetadataPlatform"},"type":"array","title":"Platforms"},"multiplayer_modes":{"items":{"$ref":"#/components/schemas/IGDBMetadataMultiplayerMode"},"type":"array","title":"Multiplayer Modes"},"player_count":{"type":"string","title":"Player Count"},"expansions":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Expansions"},"dlcs":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Dlcs"},"remasters":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Remasters"},"remakes":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Remakes"},"expanded_games":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Expanded Games"},"ports":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Ports"},"similar_games":{"items":{"$ref":"#/components/schemas/IGDBRelatedGame"},"type":"array","title":"Similar Games"}},"type":"object","title":"RomIGDBMetadata"},"RomLaunchboxMetadata":{"properties":{"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"max_players":{"type":"integer","title":"Max Players"},"release_type":{"type":"string","title":"Release Type"},"cooperative":{"type":"boolean","title":"Cooperative"},"youtube_video_id":{"type":"string","title":"Youtube Video Id"},"community_rating":{"type":"number","title":"Community Rating"},"community_rating_count":{"type":"integer","title":"Community Rating Count"},"wikipedia_url":{"type":"string","title":"Wikipedia Url"},"esrb":{"type":"string","title":"Esrb"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"images":{"items":{"$ref":"#/components/schemas/LaunchboxImage"},"type":"array","title":"Images"}},"type":"object","title":"RomLaunchboxMetadata"},"RomMetadataSchema":{"properties":{"rom_id":{"type":"integer","title":"Rom Id"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"collections":{"items":{"type":"string"},"type":"array","title":"Collections"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"age_ratings":{"items":{"type":"string"},"type":"array","title":"Age Ratings"},"player_count":{"type":"string","title":"Player Count"},"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"average_rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Average Rating"}},"type":"object","required":["rom_id","genres","franchises","collections","companies","game_modes","age_ratings","player_count","first_release_date","average_rating"],"title":"RomMetadataSchema"},"RomMobyMetadata":{"properties":{"moby_score":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Moby Score"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"alternate_titles":{"items":{"type":"string"},"type":"array","title":"Alternate Titles"},"platforms":{"items":{"$ref":"#/components/schemas/MobyMetadataPlatform"},"type":"array","title":"Platforms"}},"type":"object","title":"RomMobyMetadata"},"RomRAMetadata":{"properties":{"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"achievements":{"items":{"$ref":"#/components/schemas/RAGameRomAchievement"},"type":"array","title":"Achievements"}},"type":"object","title":"RomRAMetadata"},"RomSSMetadata":{"properties":{"bezel_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bezel Url"},"box2d_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Url"},"box2d_side_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Side Url"},"box2d_back_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Back Url"},"box3d_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box3D Url"},"fanart_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fanart Url"},"fullbox_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fullbox Url"},"logo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Url"},"manual_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manual Url"},"marquee_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marquee Url"},"miximage_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Miximage Url"},"physical_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical Url"},"screenshot_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screenshot Url"},"steamgrid_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Steamgrid Url"},"title_screen_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title Screen Url"},"video_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Url"},"video_normalized_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Normalized Url"},"bezel_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bezel Path"},"box2d_back_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box2D Back Path"},"box3d_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box3D Path"},"fanart_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fanart Path"},"miximage_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Miximage Path"},"physical_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical Path"},"marquee_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marquee Path"},"logo_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Path"},"video_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Path"},"ss_score":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ss Score"},"first_release_date":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Release Date"},"alternative_names":{"items":{"type":"string"},"type":"array","title":"Alternative Names"},"companies":{"items":{"type":"string"},"type":"array","title":"Companies"},"franchises":{"items":{"type":"string"},"type":"array","title":"Franchises"},"game_modes":{"items":{"type":"string"},"type":"array","title":"Game Modes"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"player_count":{"type":"string","title":"Player Count"}},"type":"object","title":"RomSSMetadata"},"RomUserSchema":{"properties":{"id":{"type":"integer","title":"Id"},"user_id":{"type":"integer","title":"User Id"},"rom_id":{"type":"integer","title":"Rom Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"last_played":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Played"},"is_main_sibling":{"type":"boolean","title":"Is Main Sibling"},"backlogged":{"type":"boolean","title":"Backlogged"},"now_playing":{"type":"boolean","title":"Now Playing"},"hidden":{"type":"boolean","title":"Hidden"},"rating":{"type":"integer","title":"Rating"},"difficulty":{"type":"integer","title":"Difficulty"},"completion":{"type":"integer","title":"Completion"},"status":{"anyOf":[{"$ref":"#/components/schemas/RomUserStatus"},{"type":"null"}]}},"type":"object","required":["id","user_id","rom_id","created_at","updated_at","last_played","is_main_sibling","backlogged","now_playing","hidden","rating","difficulty","completion","status"],"title":"RomUserSchema"},"RomUserStatus":{"type":"string","enum":["incomplete","finished","completed_100","retired","never_playing"],"title":"RomUserStatus"},"RoomsResponse":{"properties":{"room_name":{"type":"string","title":"Room Name"},"current":{"type":"integer","title":"Current"},"max":{"type":"integer","title":"Max"},"player_name":{"type":"string","title":"Player Name"},"hasPassword":{"type":"boolean","title":"Haspassword"}},"type":"object","required":["room_name","current","max","player_name","hasPassword"],"title":"RoomsResponse"},"SGDBResource":{"properties":{"thumb":{"type":"string","title":"Thumb"},"url":{"type":"string","title":"Url"},"type":{"type":"string","title":"Type"}},"type":"object","required":["thumb","url","type"],"title":"SGDBResource"},"SaveSchema":{"properties":{"id":{"type":"integer","title":"Id"},"rom_id":{"type":"integer","title":"Rom Id"},"user_id":{"type":"integer","title":"User Id"},"file_name":{"type":"string","title":"File Name"},"file_name_no_tags":{"type":"string","title":"File Name No Tags"},"file_name_no_ext":{"type":"string","title":"File Name No Ext"},"file_extension":{"type":"string","title":"File Extension"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"download_path":{"type":"string","title":"Download Path"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"emulator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emulator"},"slot":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slot"},"content_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Hash"},"screenshot":{"anyOf":[{"$ref":"#/components/schemas/ScreenshotSchema"},{"type":"null"}]},"device_syncs":{"items":{"$ref":"#/components/schemas/DeviceSyncSchema"},"type":"array","title":"Device Syncs","default":[]}},"type":"object","required":["id","rom_id","user_id","file_name","file_name_no_tags","file_name_no_ext","file_extension","file_path","file_size_bytes","full_path","download_path","missing_from_fs","created_at","updated_at","emulator","screenshot"],"title":"SaveSchema"},"SaveSummarySchema":{"properties":{"total_count":{"type":"integer","title":"Total Count"},"slots":{"items":{"$ref":"#/components/schemas/SlotSummarySchema"},"type":"array","title":"Slots"}},"type":"object","required":["total_count","slots"],"title":"SaveSummarySchema"},"ScanStats":{"properties":{"total_platforms":{"type":"integer","title":"Total Platforms"},"total_roms":{"type":"integer","title":"Total Roms"},"scanned_platforms":{"type":"integer","title":"Scanned Platforms"},"new_platforms":{"type":"integer","title":"New Platforms"},"identified_platforms":{"type":"integer","title":"Identified Platforms"},"scanned_roms":{"type":"integer","title":"Scanned Roms"},"new_roms":{"type":"integer","title":"New Roms"},"identified_roms":{"type":"integer","title":"Identified Roms"},"scanned_firmware":{"type":"integer","title":"Scanned Firmware"},"new_firmware":{"type":"integer","title":"New Firmware"}},"type":"object","required":["total_platforms","total_roms","scanned_platforms","new_platforms","identified_platforms","scanned_roms","new_roms","identified_roms","scanned_firmware","new_firmware"],"title":"ScanStats"},"ScanTaskMeta":{"properties":{"scan_stats":{"anyOf":[{"$ref":"#/components/schemas/ScanStats"},{"type":"null"}]}},"type":"object","required":["scan_stats"],"title":"ScanTaskMeta"},"ScanTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"scan","title":"Task Type"},"meta":{"$ref":"#/components/schemas/ScanTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"ScanTaskStatusResponse"},"ScreenshotSchema":{"properties":{"id":{"type":"integer","title":"Id"},"rom_id":{"type":"integer","title":"Rom Id"},"user_id":{"type":"integer","title":"User Id"},"file_name":{"type":"string","title":"File Name"},"file_name_no_tags":{"type":"string","title":"File Name No Tags"},"file_name_no_ext":{"type":"string","title":"File Name No Ext"},"file_extension":{"type":"string","title":"File Extension"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"download_path":{"type":"string","title":"Download Path"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","rom_id","user_id","file_name","file_name_no_tags","file_name_no_ext","file_extension","file_path","file_size_bytes","full_path","download_path","missing_from_fs","created_at","updated_at"],"title":"ScreenshotSchema"},"SearchCoverSchema":{"properties":{"name":{"type":"string","title":"Name"},"resources":{"items":{"$ref":"#/components/schemas/SGDBResource"},"type":"array","title":"Resources"}},"type":"object","required":["name","resources"],"title":"SearchCoverSchema"},"SearchRomSchema":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"igdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Igdb Id"},"moby_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Moby Id"},"ss_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ss Id"},"sgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sgdb Id"},"flashpoint_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flashpoint Id"},"launchbox_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Launchbox Id"},"platform_id":{"type":"integer","title":"Platform Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug","default":""},"summary":{"type":"string","title":"Summary","default":""},"igdb_url_cover":{"type":"string","title":"Igdb Url Cover","default":""},"moby_url_cover":{"type":"string","title":"Moby Url Cover","default":""},"ss_url_cover":{"type":"string","title":"Ss Url Cover","default":""},"sgdb_url_cover":{"type":"string","title":"Sgdb Url Cover","default":""},"flashpoint_url_cover":{"type":"string","title":"Flashpoint Url Cover","default":""},"launchbox_url_cover":{"type":"string","title":"Launchbox Url Cover","default":""},"is_unidentified":{"type":"boolean","title":"Is Unidentified"},"is_identified":{"type":"boolean","title":"Is Identified"}},"type":"object","required":["platform_id","name","is_unidentified","is_identified"],"title":"SearchRomSchema"},"SiblingRomSchema":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"fs_name_no_tags":{"type":"string","title":"Fs Name No Tags"},"fs_name_no_ext":{"type":"string","title":"Fs Name No Ext"},"sort_comparator":{"type":"string","title":"Sort Comparator","readOnly":true}},"type":"object","required":["id","name","fs_name_no_tags","fs_name_no_ext","sort_comparator"],"title":"SiblingRomSchema"},"SimpleRomSchema":{"properties":{"id":{"type":"integer","title":"Id"},"igdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Igdb Id"},"sgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sgdb Id"},"moby_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Moby Id"},"ss_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ss Id"},"ra_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ra Id"},"launchbox_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Launchbox Id"},"hasheous_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hasheous Id"},"tgdb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tgdb Id"},"flashpoint_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flashpoint Id"},"hltb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hltb Id"},"gamelist_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gamelist Id"},"platform_id":{"type":"integer","title":"Platform Id"},"platform_slug":{"type":"string","title":"Platform Slug"},"platform_fs_slug":{"type":"string","title":"Platform Fs Slug"},"platform_custom_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform Custom Name"},"platform_display_name":{"type":"string","title":"Platform Display Name"},"fs_name":{"type":"string","title":"Fs Name"},"fs_name_no_tags":{"type":"string","title":"Fs Name No Tags"},"fs_name_no_ext":{"type":"string","title":"Fs Name No Ext"},"fs_extension":{"type":"string","title":"Fs Extension"},"fs_path":{"type":"string","title":"Fs Path"},"fs_size_bytes":{"type":"integer","title":"Fs Size Bytes"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"alternative_names":{"items":{"type":"string"},"type":"array","title":"Alternative Names"},"youtube_video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Video Id"},"metadatum":{"$ref":"#/components/schemas/RomMetadataSchema"},"igdb_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomIGDBMetadata"},{"type":"null"}]},"moby_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomMobyMetadata"},{"type":"null"}]},"ss_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomSSMetadata"},{"type":"null"}]},"launchbox_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomLaunchboxMetadata"},{"type":"null"}]},"hasheous_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomHasheousMetadata"},{"type":"null"}]},"flashpoint_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomFlashpointMetadata"},{"type":"null"}]},"hltb_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomHLTBMetadata"},{"type":"null"}]},"gamelist_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomGamelistMetadata"},{"type":"null"}]},"manual_metadata":{"anyOf":[{"$ref":"#/components/schemas/ManualMetadata"},{"type":"null"}]},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"url_cover":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Cover"},"has_manual":{"type":"boolean","title":"Has Manual"},"path_manual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Manual"},"url_manual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Manual"},"is_identifying":{"type":"boolean","title":"Is Identifying","default":false},"is_unidentified":{"type":"boolean","title":"Is Unidentified"},"is_identified":{"type":"boolean","title":"Is Identified"},"revision":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revision"},"regions":{"items":{"type":"string"},"type":"array","title":"Regions"},"languages":{"items":{"type":"string"},"type":"array","title":"Languages"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"crc_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crc Hash"},"md5_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5 Hash"},"sha1_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sha1 Hash"},"ra_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ra Hash"},"has_simple_single_file":{"type":"boolean","title":"Has Simple Single File"},"has_nested_single_file":{"type":"boolean","title":"Has Nested Single File"},"has_multiple_files":{"type":"boolean","title":"Has Multiple Files"},"files":{"items":{"$ref":"#/components/schemas/RomFileSchema"},"type":"array","title":"Files"},"full_path":{"type":"string","title":"Full Path"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"has_notes":{"type":"boolean","title":"Has Notes"},"siblings":{"items":{"$ref":"#/components/schemas/SiblingRomSchema"},"type":"array","title":"Siblings"},"rom_user":{"$ref":"#/components/schemas/RomUserSchema"},"merged_screenshots":{"items":{"type":"string"},"type":"array","title":"Merged Screenshots"},"merged_ra_metadata":{"anyOf":[{"$ref":"#/components/schemas/RomRAMetadata"},{"type":"null"}]}},"type":"object","required":["id","igdb_id","sgdb_id","moby_id","ss_id","ra_id","launchbox_id","hasheous_id","tgdb_id","flashpoint_id","hltb_id","gamelist_id","platform_id","platform_slug","platform_fs_slug","platform_custom_name","platform_display_name","fs_name","fs_name_no_tags","fs_name_no_ext","fs_extension","fs_path","fs_size_bytes","name","slug","summary","alternative_names","youtube_video_id","metadatum","igdb_metadata","moby_metadata","ss_metadata","launchbox_metadata","hasheous_metadata","flashpoint_metadata","hltb_metadata","gamelist_metadata","manual_metadata","path_cover_small","path_cover_large","url_cover","has_manual","path_manual","url_manual","is_unidentified","is_identified","revision","regions","languages","tags","crc_hash","md5_hash","sha1_hash","ra_hash","has_simple_single_file","has_nested_single_file","has_multiple_files","files","full_path","created_at","updated_at","missing_from_fs","has_notes","siblings","rom_user","merged_screenshots","merged_ra_metadata"],"title":"SimpleRomSchema"},"SlotSummarySchema":{"properties":{"slot":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slot"},"count":{"type":"integer","title":"Count"},"latest":{"$ref":"#/components/schemas/SaveSchema"}},"type":"object","required":["slot","count","latest"],"title":"SlotSummarySchema"},"SmartCollectionSchema":{"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description","default":""},"rom_ids":{"items":{"type":"integer"},"type":"array","uniqueItems":true,"title":"Rom Ids"},"rom_count":{"type":"integer","title":"Rom Count"},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"path_covers_small":{"items":{"type":"string"},"type":"array","title":"Path Covers Small"},"path_covers_large":{"items":{"type":"string"},"type":"array","title":"Path Covers Large"},"is_public":{"type":"boolean","title":"Is Public","default":false},"is_favorite":{"type":"boolean","title":"Is Favorite","default":false},"is_virtual":{"type":"boolean","title":"Is Virtual","default":false},"is_smart":{"type":"boolean","title":"Is Smart","default":true},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"id":{"type":"integer","title":"Id"},"filter_criteria":{"additionalProperties":true,"type":"object","title":"Filter Criteria"},"filter_summary":{"type":"string","title":"Filter Summary"},"user_id":{"type":"integer","title":"User Id"},"owner_username":{"type":"string","title":"Owner Username"}},"type":"object","required":["name","rom_ids","rom_count","path_cover_small","path_cover_large","path_covers_small","path_covers_large","created_at","updated_at","id","filter_criteria","filter_summary","user_id","owner_username"],"title":"SmartCollectionSchema"},"StateSchema":{"properties":{"id":{"type":"integer","title":"Id"},"rom_id":{"type":"integer","title":"Rom Id"},"user_id":{"type":"integer","title":"User Id"},"file_name":{"type":"string","title":"File Name"},"file_name_no_tags":{"type":"string","title":"File Name No Tags"},"file_name_no_ext":{"type":"string","title":"File Name No Ext"},"file_extension":{"type":"string","title":"File Extension"},"file_path":{"type":"string","title":"File Path"},"file_size_bytes":{"type":"integer","title":"File Size Bytes"},"full_path":{"type":"string","title":"Full Path"},"download_path":{"type":"string","title":"Download Path"},"missing_from_fs":{"type":"boolean","title":"Missing From Fs"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"emulator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emulator"},"screenshot":{"anyOf":[{"$ref":"#/components/schemas/ScreenshotSchema"},{"type":"null"}]}},"type":"object","required":["id","rom_id","user_id","file_name","file_name_no_tags","file_name_no_ext","file_extension","file_path","file_size_bytes","full_path","download_path","missing_from_fs","created_at","updated_at","emulator","screenshot"],"title":"StateSchema"},"StatsReturn":{"properties":{"PLATFORMS":{"type":"integer","title":"Platforms"},"ROMS":{"type":"integer","title":"Roms"},"SAVES":{"type":"integer","title":"Saves"},"STATES":{"type":"integer","title":"States"},"SCREENSHOTS":{"type":"integer","title":"Screenshots"},"TOTAL_FILESIZE_BYTES":{"type":"integer","title":"Total Filesize Bytes"}},"type":"object","required":["PLATFORMS","ROMS","SAVES","STATES","SCREENSHOTS","TOTAL_FILESIZE_BYTES"],"title":"StatsReturn"},"SyncMode":{"type":"string","enum":["api","file_transfer","push_pull"],"title":"SyncMode"},"SystemDict":{"properties":{"VERSION":{"type":"string","title":"Version"},"SHOW_SETUP_WIZARD":{"type":"boolean","title":"Show Setup Wizard"}},"type":"object","required":["VERSION","SHOW_SETUP_WIZARD"],"title":"SystemDict"},"TaskExecutionResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at"],"title":"TaskExecutionResponse"},"TaskInfo":{"properties":{"name":{"type":"string","title":"Name"},"type":{"$ref":"#/components/schemas/TaskType"},"manual_run":{"type":"boolean","title":"Manual Run"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"enabled":{"type":"boolean","title":"Enabled"},"cron_string":{"type":"string","title":"Cron String"}},"type":"object","required":["name","type","manual_run","title","description","enabled","cron_string"],"title":"TaskInfo"},"TaskType":{"type":"string","enum":["scan","conversion","cleanup","update","watcher","generic"],"title":"TaskType","description":"Enumeration of task types for categorization and UI display."},"TasksDict":{"properties":{"ENABLE_SCHEDULED_RESCAN":{"type":"boolean","title":"Enable Scheduled Rescan"},"SCHEDULED_RESCAN_CRON":{"type":"string","title":"Scheduled Rescan Cron"},"ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB":{"type":"boolean","title":"Enable Scheduled Update Switch Titledb"},"SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON":{"type":"string","title":"Scheduled Update Switch Titledb Cron"},"ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA":{"type":"boolean","title":"Enable Scheduled Update Launchbox Metadata"},"SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON":{"type":"string","title":"Scheduled Update Launchbox Metadata Cron"},"ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP":{"type":"boolean","title":"Enable Scheduled Convert Images To Webp"},"SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRON":{"type":"string","title":"Scheduled Convert Images To Webp Cron"}},"type":"object","required":["ENABLE_SCHEDULED_RESCAN","SCHEDULED_RESCAN_CRON","ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB","SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON","ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA","SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON","ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP","SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRON"],"title":"TasksDict"},"TinfoilFeedFileSchema":{"properties":{"url":{"type":"string","title":"Url"},"size":{"type":"integer","title":"Size"}},"type":"object","required":["url","size"],"title":"TinfoilFeedFileSchema"},"TinfoilFeedSchema":{"properties":{"files":{"items":{"$ref":"#/components/schemas/TinfoilFeedFileSchema"},"type":"array","title":"Files"},"directories":{"items":{"type":"string"},"type":"array","title":"Directories"},"titledb":{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object","title":"Titledb"},"success":{"type":"string","title":"Success"},"error":{"type":"string","title":"Error"}},"type":"object","required":["files","directories"],"title":"TinfoilFeedSchema"},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"token_type":{"type":"string","title":"Token Type"},"expires":{"type":"integer","title":"Expires"}},"type":"object","required":["access_token","token_type","expires"],"title":"TokenResponse"},"UpdateStats":{"properties":{"processed":{"type":"integer","title":"Processed"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["processed","total"],"title":"UpdateStats"},"UpdateTaskMeta":{"properties":{"update_stats":{"anyOf":[{"$ref":"#/components/schemas/UpdateStats"},{"type":"null"}]}},"type":"object","required":["update_stats"],"title":"UpdateTaskMeta"},"UpdateTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"update","title":"Task Type"},"meta":{"$ref":"#/components/schemas/UpdateTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"UpdateTaskStatusResponse"},"UserCollectionSchema":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["id","name"],"title":"UserCollectionSchema"},"UserForm":{"properties":{"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"},"ra_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ra Username"},"avatar":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Avatar"},"ui_settings":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ui Settings"}},"type":"object","title":"UserForm"},"UserNoteSchema":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"type":"string","title":"Title"},"content":{"type":"string","title":"Content"},"is_public":{"type":"boolean","title":"Is Public"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"user_id":{"type":"integer","title":"User Id"},"username":{"type":"string","title":"Username"}},"type":"object","required":["id","title","content","is_public","created_at","updated_at","user_id","username"],"title":"UserNoteSchema"},"UserSchema":{"properties":{"id":{"type":"integer","title":"Id"},"username":{"type":"string","title":"Username"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"enabled":{"type":"boolean","title":"Enabled"},"role":{"$ref":"#/components/schemas/Role"},"oauth_scopes":{"items":{"type":"string"},"type":"array","title":"Oauth Scopes"},"avatar_path":{"type":"string","title":"Avatar Path"},"last_login":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Login"},"last_active":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Active"},"ra_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ra Username"},"ra_progression":{"anyOf":[{"$ref":"#/components/schemas/RAProgression"},{"type":"null"}]},"ui_settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Ui Settings"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","username","email","enabled","role","oauth_scopes","avatar_path","last_login","last_active","created_at","updated_at"],"title":"UserSchema"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VirtualCollectionSchema":{"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"rom_ids":{"items":{"type":"integer"},"type":"array","uniqueItems":true,"title":"Rom Ids"},"rom_count":{"type":"integer","title":"Rom Count"},"path_cover_small":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Small"},"path_cover_large":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Cover Large"},"path_covers_small":{"items":{"type":"string"},"type":"array","title":"Path Covers Small"},"path_covers_large":{"items":{"type":"string"},"type":"array","title":"Path Covers Large"},"is_public":{"type":"boolean","title":"Is Public","default":false},"is_favorite":{"type":"boolean","title":"Is Favorite","default":false},"is_virtual":{"type":"boolean","title":"Is Virtual","default":true},"is_smart":{"type":"boolean","title":"Is Smart","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"id":{"type":"string","title":"Id"},"type":{"type":"string","title":"Type"}},"type":"object","required":["name","description","rom_ids","rom_count","path_cover_small","path_cover_large","path_covers_small","path_covers_large","created_at","updated_at","id","type"],"title":"VirtualCollectionSchema"},"WatcherTaskMeta":{"properties":{},"type":"object","title":"WatcherTaskMeta"},"WatcherTaskStatusResponse":{"properties":{"task_name":{"type":"string","title":"Task Name"},"task_id":{"type":"string","title":"Task Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"enqueued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enqueued At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"task_type":{"type":"string","const":"watcher","title":"Task Type"},"meta":{"$ref":"#/components/schemas/WatcherTaskMeta"}},"type":"object","required":["task_name","task_id","status","created_at","enqueued_at","started_at","ended_at","task_type","meta"],"title":"WatcherTaskStatusResponse"},"WebrcadeFeedCategorySchema":{"properties":{"title":{"type":"string","title":"Title"},"longTitle":{"type":"string","title":"Longtitle"},"background":{"type":"string","title":"Background"},"thumbnail":{"type":"string","title":"Thumbnail"},"description":{"type":"string","title":"Description"},"items":{"items":{"$ref":"#/components/schemas/WebrcadeFeedItemSchema"},"type":"array","title":"Items"}},"type":"object","required":["title","items"],"title":"WebrcadeFeedCategorySchema"},"WebrcadeFeedItemPropsSchema":{"properties":{"rom":{"type":"string","title":"Rom"}},"type":"object","required":["rom"],"title":"WebrcadeFeedItemPropsSchema"},"WebrcadeFeedItemSchema":{"properties":{"title":{"type":"string","title":"Title"},"longTitle":{"type":"string","title":"Longtitle"},"description":{"type":"string","title":"Description"},"type":{"type":"string","title":"Type"},"thumbnail":{"type":"string","title":"Thumbnail"},"background":{"type":"string","title":"Background"},"props":{"$ref":"#/components/schemas/WebrcadeFeedItemPropsSchema"}},"type":"object","required":["title","type","props"],"title":"WebrcadeFeedItemSchema"},"WebrcadeFeedSchema":{"properties":{"title":{"type":"string","title":"Title"},"longTitle":{"type":"string","title":"Longtitle"},"description":{"type":"string","title":"Description"},"thumbnail":{"type":"string","title":"Thumbnail"},"background":{"type":"string","title":"Background"},"categories":{"items":{"$ref":"#/components/schemas/WebrcadeFeedCategorySchema"},"type":"array","title":"Categories"}},"type":"object","required":["title","categories"],"title":"WebrcadeFeedSchema"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{"me.read":"View your profile","roms.read":"View ROMs","platforms.read":"View platforms","assets.read":"View assets","devices.read":"View devices","firmware.read":"View firmware","roms.user.read":"View user-rom properties","collections.read":"View collections","me.write":"Modify your profile","assets.write":"Modify assets","devices.write":"Modify devices","roms.user.write":"Modify user-rom properties","collections.write":"Modify collections","roms.write":"Modify ROMs","platforms.write":"Modify platforms","firmware.write":"Modify firmware","users.read":"View users","users.write":"Modify users","tasks.run":"Run tasks"},"tokenUrl":"/token"}}},"HTTPBasic":{"type":"http","scheme":"basic"}}}} \ No newline at end of file diff --git a/src/bun/api/app.ts b/src/bun/api/app.ts index ac4e780..4695726 100644 --- a/src/bun/api/app.ts +++ b/src/bun/api/app.ts @@ -1,90 +1,126 @@ -import { TaskQueue } from "./task-queue"; +import { TaskQueue, AppEventMap } from "@simeonradivoev/gameflow-sdk"; import { Database } from "bun:sqlite"; import { CookieJar } from 'tough-cookie'; import FileCookieStore from 'tough-cookie-file-store'; import path from 'node:path'; import { migrate } from "drizzle-orm/bun-sqlite/migrator"; -import { drizzle } from "drizzle-orm/bun-sqlite"; +import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite"; import Conf from "conf"; import projectPackage from '~/package.json'; -import { Notification, SettingsSchema, SettingsType } from "@shared/constants"; +import { SettingsType, SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared'; import { client } from "@clients/romm/client.gen"; import * as schema from "@schema/app"; import cacheSchema from "@schema/cache"; import * as emulatorSchema from "@schema/emulators"; -import { login, logout } from "./auth"; import os from 'node:os'; -import { ActiveGame } from "../types/types"; import EventEmitter from "node:events"; -import { ErrorLike } from "bun"; -import { appPath, getErrorMessage } from "../utils"; +import { appPath } from "../utils"; import { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite"; import { ensureDir } from "fs-extra"; -import UpdateStoreJob from "./jobs/update-store"; +import { PluginManager } from "./plugins/plugin-manager"; +import registerPlugins from "./plugins/register-plugins"; +import controls from './controls/controls'; +import { RunAPIServer } from "./rpc"; +import { RunBunServer } from "../server"; +import ReloadPluginsJob from "./jobs/reload-plugins-job"; -export const config = new Conf({ - projectName: projectPackage.name, - projectSuffix: 'bun', - schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any, - defaults: SettingsSchema.parse({ - downloadPath: path.join(os.homedir(), "gameflow"), - windowSize: { width: 1280, height: 800 } - } satisfies SettingsType), -}); -export const customEmulators = new Conf>({ - projectName: projectPackage.name, - projectSuffix: 'bun', - configName: 'custom-emulators', - rootSchema: { - "type": "object", - "additionalProperties": { - "type": "string" - } - } -}); - -console.log("Config Path Located At: ", config.path); -console.log("Custom Emulator Paths Located At: ", customEmulators.path); -console.log("App Directory is ", process.env.APPDIR); -const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json')); -console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath); -export const jar = new CookieJar(fileCookieStore); +export let config: Conf; +export let customEmulators: Conf>; +export let fileCookieStore: FileCookieStore; +export let jar: CookieJar; let sqlite: Database; -export const cachePath = path.join(os.tmpdir(), 'gameflow', 'cache.sqlite'); +export let cachePath: string; let cacheSqlite: Database; export let db: DrizzleSqliteDODatabase; export let cache: DrizzleSqliteDODatabase; -await reloadDatabase(); -const emulatorsSqlite = new Database(appPath(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`), { readonly: true }); -export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema }); -export const taskQueue = new TaskQueue(); -config.onDidChange('rommAddress', v => client.setConfig({ baseUrl: v })); -await login(); -export let activeGame: ActiveGame | undefined; -export function setActiveGame (game: ActiveGame) +let emulatorsSqlite: Database; +export let emulatorsDb: BunSQLiteDatabase & { $client: Database; }; +export let taskQueue: TaskQueue; +export let plugins: PluginManager; +export let events: EventEmitter; +let controlsHandle: { cleanup: () => void; }; +let api: { cleanup: () => Promise; }; +let bunServer: { cleanup: () => Promise; } | undefined; +let cleannedUp = false; +let cleaningUp = false; + +export async function load () { - if (activeGame) throw new Error("Only one active game at a time"); - return activeGame = game; + config = new Conf({ + projectName: projectPackage.name, + projectSuffix: 'bun', + cwd: process.env.CONFIG_CWD, + schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any, + defaults: SettingsSchema.parse({ + downloadPath: process.env.DEFAULT_DOWNLOAD_PATH ?? path.join(os.homedir(), "gameflow"), + windowSize: { width: 1280, height: 800 } + }), + }); + + customEmulators = new Conf>({ + projectName: projectPackage.name, + projectSuffix: 'bun', + cwd: process.env.CONFIG_CWD, + configName: 'custom-emulators', + rootSchema: { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }); + + console.log("Config Path Located At: ", config.path); + console.log("Custom Emulator Paths Located At: ", customEmulators.path); + console.log("App Directory is ", process.env.APPDIR); + console.log("Cache Path is ", cachePath); + + cachePath = path.join(os.tmpdir(), 'gameflow', 'cache.sqlite'); + fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json')); + console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath); + jar = new CookieJar(fileCookieStore); + taskQueue = new TaskQueue(); + events = new EventEmitter(); + emulatorsSqlite = new Database(appPath(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`), { readonly: true }); + emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema }); + await reloadDatabase(); + plugins = new PluginManager(); + api = await RunAPIServer(); + await registerPlugins(plugins); + taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); + controlsHandle = await controls(); + if (!process.env.PUBLIC_ACCESS) bunServer = await RunBunServer(); + + config.onDidChange('downloadPath', () => reloadDatabase()); + config.onDidChange('rommAddress', v => client.setConfig({ baseUrl: v })); } -export const events = new EventEmitter(); -events.addListener('activegameexit', ({ error }) => -{ - activeGame = undefined; - if (error) - { - events.emit('notification', { message: getErrorMessage(error), type: 'error' }); - } -}); -config.onDidChange('downloadPath', () => reloadDatabase()); -taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob()); export async function cleanup () { + if (cleaningUp) throw new Error("Already Cleaning Up"); + cleaningUp = true; + if (cleannedUp) throw new Error("Already Cleaned Up. Skipping"); + console.log("Cleaning Up"); + await bunServer?.cleanup(); + await api.cleanup(); await taskQueue.close(); - sqlite.close(); - await logout(); + await plugins.cleanup(); + controlsHandle.cleanup(); + cacheSqlite.close(); emulatorsSqlite.close(); + sqlite.close(); + config._closeWatcher(); + customEmulators._closeWatcher(); + console.log("Finished Cleaning Up"); + cleannedUp = true; +} + +/** Reset the cleanup flags. This is mainly used by tests since they run the same app. */ +export async function resetCleanup () +{ + cleaningUp = false; + cleannedUp = false; } export async function reloadDatabase () @@ -97,7 +133,8 @@ export async function reloadDatabase () db = drizzle(sqlite, { schema }); cache = drizzle(cacheSqlite, { schema: cacheSchema }); migrate(db!, { migrationsFolder: appPath("./drizzle") }); - cache.run(` + sqlite.run("PRAGMA foreign_keys = ON;"); + await cache.run(` CREATE TABLE IF NOT EXISTS item_cache ( key TEXT PRIMARY KEY, data TEXT NOT NULL, @@ -107,9 +144,3 @@ export async function reloadDatabase () `); } -interface AppEventMap -{ - activegameexit: [{ source: string, id: string, subprocess?: Bun.Subprocess, exitCode: number | null, signalCode: number | null, error?: ErrorLike; }]; - exitapp: []; - notification: [Notification]; -} \ No newline at end of file diff --git a/src/bun/api/auth.ts b/src/bun/api/auth.ts index 501275d..2cd6e3f 100644 --- a/src/bun/api/auth.ts +++ b/src/bun/api/auth.ts @@ -1,8 +1,7 @@ -import Elysia, { sse, status } from "elysia"; -import { config, events, jar, taskQueue } from "./app"; +import Elysia, { status } from "elysia"; +import { config, events, plugins, taskQueue } from "./app"; import z from "zod"; -import { client } from "@clients/romm/client.gen"; -import { loginApiLoginPost, logoutApiLogoutPost } from "@clients/romm"; +import { getCurrentUserApiUsersMeGet, tokenApiTokenPost, UserSchema } from "@clients/romm"; import secrets from '../api/secrets'; import { LoginJob } from "./jobs/login-job"; import TwitchLoginJob from "./jobs/twitch-login-job"; @@ -43,68 +42,12 @@ export default new Elysia() await secrets.delete({ service: 'gamflow_twitch', name: 'refresh_token' }); await secrets.delete({ service: 'gamflow_twitch', name: 'expires_in' }); + await plugins.hooks.auth.loginComplete.promise({ service: 'twitch' }); + return status(res.status, res.statusText); }) - .get('/login/twitch', async () => - { - const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); - if (!access_token) - { - return status('Not Found', "Not Logged In"); - } - - const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${access_token}` } }); - if (res.ok) - { - return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; }; - } - - if (!process.env.TWITCH_CLIENT_ID) - { - return status("Not Found", "Twitch Client ID not set"); - } - - const refresh_token = await secrets.get({ service: 'gamflow_twitch', name: "refresh_token" }); - if (!refresh_token) - { - return status("Not Found", "Refresh Token Not Found"); - } - - // refresh token - const refreshResponse = await fetch('https://id.twitch.tv/oauth2/token', { - method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ - client_id: process.env.TWITCH_CLIENT_ID, - access_token, - grant_type: "refresh_token", - refresh_token - }) - }); - - if (refreshResponse.ok) - { - const data: { - access_token: string, - refresh_token: string, - token_type: string; - expires_in: number; - } = await refreshResponse.json(); - - await secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token }); - await secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token }); - await secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() }); - - events.emit('notification', { message: "Twitch Refresh Successful", type: 'success' }); - - const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${data.access_token}` } }); - if (res.ok) - { - return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; }; - } - } - - return status(400, res.statusText); - }) - .post('/login/romm', async () => + .get('/login/twitch', checkLoginAndRefreshTwitch) + .post('/login/romm/qr', async () => { if (taskQueue.hasActiveOfType(LoginJob)) { @@ -113,117 +56,156 @@ export default new Elysia() return taskQueue.enqueue(LoginJob.id, new LoginJob()); }) - .post('/login', async ({ body }) => tryLoginAndSave(body), { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) }) - .get('/login', async () => + .get('/user/romm', async () => { - const credentials = await secrets.get({ service: 'gameflow', name: 'romm' }); - return { hasPassword: !!credentials }; - }, { response: z.object({ hasPassword: z.boolean() }) }) - .post('/logout', async () => + const data = await getCurrentUserApiUsersMeGet(); + if (data.error) return status("Internal Server Error", data.response.statusText); + return data.data as UserSchema; + }) + .post('/login/romm', async ({ body }) => tryLoginAndSave(body), { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) }) + .get('/login/romm', checkLoginAndRefreshRomm, + { response: z.object({ hasLogin: z.boolean() }) }) + .post('/logout/romm', async () => { - await secrets.delete({ service: 'gameflow', name: 'romm' }); - await logout(); - const rommAddress = config.get('rommAddress'); - if (rommAddress) - { - const cookies = await jar.getCookies(rommAddress); - cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key)); - } + await secrets.delete({ service: 'gameflow', name: 'romm_access_token' }); + await secrets.delete({ service: 'gameflow', name: 'romm_refresh_token' }); + await secrets.delete({ service: 'gameflow', name: 'romm_expires_in' }); return status(200); }, { response: z.any() }); -async function updateClient () + +export async function checkLoginAndRefreshTwitch () { - client.setConfig({ - baseUrl: config.get('rommAddress'), headers: { - cookie: await jar.getCookieString(config.get('rommAddress') ?? '') - } + const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); + if (!access_token) + { + return status('Not Found', "Not Logged In"); + } + + const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${access_token}` } }); + if (res.ok) + { + return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; }; + } + + if (!process.env.TWITCH_CLIENT_ID) + { + return status("Not Found", "Twitch Client ID not set"); + } + + const refresh_token = await secrets.get({ service: 'gamflow_twitch', name: "refresh_token" }); + if (!refresh_token) + { + return status("Not Found", "Refresh Token Not Found"); + } + + // refresh token + const refreshResponse = await fetch('https://id.twitch.tv/oauth2/token', { + method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ + client_id: process.env.TWITCH_CLIENT_ID, + access_token, + grant_type: "refresh_token", + refresh_token + }) }); + + if (refreshResponse.ok) + { + const data: { + access_token: string, + refresh_token: string, + token_type: string; + expires_in: number; + } = await refreshResponse.json(); + + await secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token }); + await secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token }); + await secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() }); + + await plugins.hooks.auth.loginComplete.promise({ service: 'twitch' }); + + events.emit('notification', { message: "Twitch Refresh Successful", type: 'success' }); + + const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${data.access_token}` } }); + if (res.ok) + { + return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; }; + } + } + + return status(400, res.statusText); +} + +export async function checkLoginAndRefreshRomm () +{ + //TODO: move to plugin logic + if (plugins.plugins['com.simeonradivoev.gameflow.romm'].config?.get('clientApiToken')) + { + return { hasLogin: true }; + } + + const access_token = await secrets.get({ service: 'gameflow', name: 'romm_access_token' }); + if (!access_token) + { + return { hasLogin: false }; + } + + const expires_in = await secrets.get({ service: 'gameflow', name: "romm_expires_in" }); + if (expires_in) + { + const date = new Date(expires_in); + if (date > new Date()) + { + return { hasLogin: true }; + } + } + + const refresh_token = await secrets.get({ service: 'gameflow', name: "romm_refresh_token" }); + if (!refresh_token) + { + return { hasLogin: false }; + } + + const refreshResponse = await tokenApiTokenPost({ body: { grant_type: "refresh_token", refresh_token: refresh_token } }); + + if (refreshResponse.response.ok && refreshResponse.data) + { + await secrets.set({ service: 'gameflow', name: 'romm_access_token', value: refreshResponse.data.access_token }); + if (refreshResponse.data.refresh_token) + await secrets.set({ service: 'gameflow', name: 'romm_refresh_token', value: refreshResponse.data.refresh_token }); + await secrets.set({ service: 'gameflow', name: 'romm_expires_in', value: new Date(new Date().getTime() + refreshResponse.data.expires * 1000).toString() }); + + await plugins.hooks.auth.loginComplete.promise({ service: 'romm' }); + + events.emit('notification', { message: "Romm Refresh Successful", type: 'success' }); + return { hasLogin: true }; + } + + return status(refreshResponse.response.status, refreshResponse.response.statusText) as any; } export async function tryLoginAndSave ({ host, username, password }: { host: string, username: string, password: string; }) { - if (config.has('rommAddress') && config.has('rommUser')) + const response = await tokenApiTokenPost({ + body: { + password, + username, + scope: 'me.read roms.read platforms.read assets.read assets.write firmware.read roms.user.read collections.read me.write roms.user.write' + }, baseUrl: host + }); + + if (response.response.ok && response.data) { - await logout(); - const oldRommAddress = config.get('rommAddress'); - if (oldRommAddress) + await secrets.set({ service: 'gameflow', name: 'romm_access_token', value: response.data.access_token }); + await secrets.set({ service: 'gameflow', name: 'romm_expires_in', value: new Date(new Date().getTime() + response.data.expires * 1000).toString() }); + if (response.data.refresh_token) { - const cookies = await jar.getCookies(oldRommAddress); - await Promise.all(cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key))); + await secrets.set({ service: 'gameflow', name: 'romm_refresh_token', value: response.data.refresh_token }); } - } - const response = await login({ rommAddress: host, rommUser: username, rommPassword: password }); - if (response?.code === 200) - { config.set('rommAddress', host); - config.set('rommUser', username); - - await secrets.set({ service: 'gameflow', name: 'romm', value: password }); + await plugins.hooks.auth.loginComplete.promise({ service: 'twitch' }); } return response; -} - -export async function logout () -{ - if (!config.has('rommAddress')) - { - return; - } - const rommAddress = config.get('rommAddress'); - if (rommAddress) - { - console.log("Logging Out of ROMM"); - try - { - await logoutApiLogoutPost({ - baseUrl: rommAddress, headers: { - 'cookie': await jar.getCookieString(rommAddress) - } - }); - await jar.store.removeCookie(new URL(rommAddress).host, null, "romm_session"); - } catch (error) - { - console.error("Failed to logout of ROMM ", error); - } - } -} - -export async function login (data?: { rommAddress?: string, rommUser?: string, rommPassword?: string; }) -{ - const address = data?.rommAddress ?? config.get('rommAddress'); - const user = data?.rommUser ?? config.get('rommUser'); - const password = data?.rommPassword ?? await secrets.get({ service: 'gameflow', name: "romm" }); - - if (!address || !user) - { - console.warn("Romm not setup"); - return status(404); - } - const rommAddress = config.get('rommAddress'); - const rommUser = config.get('rommUser'); - if (rommAddress && rommUser) - { - console.log("Logging In to ROMM"); - if (password === null) - { - return status(404, "No Found Password"); - } - - const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` }); - if (loginResponse.response.status === 200) - { - loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress)); - await updateClient(); - return status(200, loginResponse.response.statusText); - } else - { - console.error("Could not Login to Romm: ", loginResponse.response.statusText); - return status(loginResponse.response.status, loginResponse.response.statusText); - } - - } -} - +} \ No newline at end of file diff --git a/src/bun/api/cache.ts b/src/bun/api/cache.ts index c8c4a8f..04abd1e 100644 --- a/src/bun/api/cache.ts +++ b/src/bun/api/cache.ts @@ -1,6 +1,9 @@ import { eq } from "drizzle-orm"; import { cache } from "./app"; import cacheSchema from "@schema/cache"; +import { GithubReleaseSchema } from '@simeonradivoev/gameflow-sdk/shared'; +import PQueue from "p-queue"; +import z from "zod"; export const CACHE_KEYS = { ROM_PLATFORMS: 'rom-platforms', @@ -8,17 +11,21 @@ export const CACHE_KEYS = { STORE_GAME_MANIFEST: 'store-game-manifest' } as const; -export async function getOrCached (key: string, getter: () => Promise, options?: { expireMs?: number; }): Promise +// we aggressively cache github data so burst of calls is fine. +export const githubRequestQueue = new PQueue({ intervalCap: 60, interval: 1000 * 60 * 60, strict: true }); + +export async function getOrCached (key: string, getter: (lastValue: T | undefined) => Promise, options?: { expireMs?: number; force?: boolean; }): Promise { const cached = await cache.query.item_cache.findFirst({ where: eq(cacheSchema.item_cache.key, key) }); const updated_at = new Date(); - if (cached && cached.expire_at > updated_at) + if (cached && cached.expire_at > updated_at && !options?.force) { return cached.data as T; } - const data = await getter(); + const data = await getter(cached?.data as T); + if (data === undefined) return data; const expire_at = options?.expireMs ? new Date(updated_at.getTime() + options.expireMs) : new Date(updated_at.getTime() + 24 * 60 * 60 * 1000); @@ -31,4 +38,17 @@ export async function getOrCached (key: string, getter: () => Promise, opt .run(); return data; +} + +export async function getOrCachedGithubRelease (path: string, forceCheck?: boolean) +{ + return getOrCached>(`github-release-${path}`, () => githubRequestQueue.add(async () => + { + const response = await fetch(`https://api.github.com/repos/${path}/releases/latest`, { + method: "GET" + }); + if (!response.ok) throw new Error(response.statusText); + const release = await GithubReleaseSchema.parseAsync(await response.json()); + return release; + }), { expireMs: 1000 * 60 * 60, force: forceCheck }); } \ No newline at end of file diff --git a/src/bun/api/clients.ts b/src/bun/api/clients.ts index b444eb6..470faf8 100644 --- a/src/bun/api/clients.ts +++ b/src/bun/api/clients.ts @@ -4,11 +4,15 @@ import { config, jar } from "./app"; import games from "./games/games"; import platforms from "./games/platforms"; import auth from "./auth"; +import collections from "./games/collections"; +import emulatorjs from "./emulatorjs/emulatorjs"; export default new Elysia({ prefix: "/api/romm" }) - .use([games, platforms, auth]) - .all("/*", async ({ request, params, set }) => + .use([games, platforms, collections, auth, emulatorjs]) + .all("/*", async ({ request, set }) => { + set.headers["cross-origin-resource-policy"] = 'cross-origin'; + if (!config.has('rommAddress') && !config.get('rommAddress')) { return new Response("Romm Address Not Found", { status: 404 }); diff --git a/src/bun/api/controls/controls.ts b/src/bun/api/controls/controls.ts new file mode 100644 index 0000000..cc3c455 --- /dev/null +++ b/src/bun/api/controls/controls.ts @@ -0,0 +1,66 @@ +import { LaunchGameJob } from '../jobs/launch-game-job'; +import { events, taskQueue } from '../app'; +import { GamepadManager } from './manager'; + +export default async function Initialize () +{ + let startSelectPressed = false; + let endPressed = false; + + const manager = new GamepadManager(); + + function handleFocus () + { + const launchGameTask = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob); + if (launchGameTask) + { + taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300)); + launchGameTask.abort('exit'); + } else + { + events.emit('focus'); + } + } + + const loop = setInterval(() => + { + for (const pad of manager.getGamepads()) + { + const state = pad.update(); + if (!state) continue; + + if (state.buttons.START && state.buttons.SELECT) + { + if (!startSelectPressed) + { + startSelectPressed = true; + handleFocus(); + } + } else + { + startSelectPressed = false; + } + } + + const keyboard = manager.getKeyboard(); + const keyState = keyboard.update(); + if (keyState?.keys.End && keyState?.keys.LeftControl) + { + if (!endPressed) + { + endPressed = true; + handleFocus(); + } + } else + { + endPressed = false; + } + }, 100); + + return { + cleanup: () => + { + clearInterval(loop); + } + }; +} \ No newline at end of file diff --git a/src/bun/api/controls/gamepad.ts b/src/bun/api/controls/gamepad.ts new file mode 100644 index 0000000..2589864 --- /dev/null +++ b/src/bun/api/controls/gamepad.ts @@ -0,0 +1,34 @@ +// ./gamepad/index.ts + + +import type { IGamepadBackend, GamepadState } from "./types"; + +export class Gamepad +{ + private index: number; + private backend: IGamepadBackend | undefined; + + constructor(index = 0) + { + this.index = index; + } + + async init () + { + if (process.platform === "win32") + { + const { GamepadWindows } = await import("./windows"); + this.backend = new GamepadWindows(this.index); + } + } + + update (): GamepadState | null + { + return this.backend?.update() ?? null; + } + + close () + { + this.backend?.close?.(); + } +} \ No newline at end of file diff --git a/src/bun/api/controls/keyboard.ts b/src/bun/api/controls/keyboard.ts new file mode 100644 index 0000000..7ad9df1 --- /dev/null +++ b/src/bun/api/controls/keyboard.ts @@ -0,0 +1,22 @@ +import type { IKeyboardBackend, KeyboardState } from "./types"; + +export class Keybaord +{ + private backend: IKeyboardBackend | undefined; + + async init () + { + if (process.platform === "win32") + { + const { KeyboardWindows } = await import("./windows"); + this.backend = new KeyboardWindows(); + } else + { + } + } + + update (): KeyboardState | null + { + return this.backend?.update() ?? null; + } +} \ No newline at end of file diff --git a/src/bun/api/controls/linux.ts b/src/bun/api/controls/linux.ts new file mode 100644 index 0000000..436d438 --- /dev/null +++ b/src/bun/api/controls/linux.ts @@ -0,0 +1,23 @@ +import { IGamepadBackend, GamepadState } from "./types"; + +export class GamepadLinux implements IGamepadBackend +{ + constructor(index = 0) + { + + } + + update (): GamepadState | null + { + return null; + } + + isConnected () + { + return false; + } + + close () + { + } +} \ No newline at end of file diff --git a/src/bun/api/controls/manager.ts b/src/bun/api/controls/manager.ts new file mode 100644 index 0000000..6143c20 --- /dev/null +++ b/src/bun/api/controls/manager.ts @@ -0,0 +1,65 @@ +import { Gamepad } from "./gamepad"; +import { platform } from "os"; +import { Keybaord } from "./keyboard"; + +export class GamepadManager +{ + private gamepads: Gamepad[] = []; + private keyboard: Keybaord; + private scanInterval: any; + + constructor() + { + this.scanGamepads(); + this.keyboard = new Keybaord(); + this.keyboard.init(); + // scan every second for new/disconnected devices + this.scanInterval = setInterval(async () => this.scanGamepads(), 1000); + } + + private async scanGamepads () + { + const max = platform() === "win32" ? 4 : 8; // max controllers + for (let i = 0; i < max; i++) + { + if (!this.gamepads[i]) + { + try + { + const pad = new Gamepad(i); + await pad.init(); + if (pad.update()) + { + this.gamepads[i] = pad; + console.log(`Gamepad ${i} connected`); + } + } catch { } + } else + { + const connected = this.gamepads[i].update() !== null; + if (!connected) + { + console.log(`Gamepad ${i} disconnected`); + this.gamepads[i].close(); + delete this.gamepads[i]; + } + } + } + } + + getKeyboard () + { + return this.keyboard; + } + + getGamepads () + { + return this.gamepads.filter(Boolean); + } + + stop () + { + clearInterval(this.scanInterval); + for (const pad of this.gamepads) pad.close?.(); + } +} \ No newline at end of file diff --git a/src/bun/api/controls/types.ts b/src/bun/api/controls/types.ts new file mode 100644 index 0000000..5c45b37 --- /dev/null +++ b/src/bun/api/controls/types.ts @@ -0,0 +1,53 @@ +export type ButtonName = + | "A" | "B" | "X" | "Y" + | "UP" | "DOWN" | "LEFT" | "RIGHT" + | "LB" | "RB" + | "START" | "SELECT" + | "L3" | "R3"; + +export type KeyCode = + | "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight" + | "KeyW" | "KeyA" | "KeyS" | "KeyD" + | "Enter" | "Escape" | "Space" | "End" | "LeftShift" | "RightShift" | "LeftControl" | "RightControl" | "LeftAlt" | "RightAlt"; + +export interface KeyboardState +{ + keys: Record; +} + +export interface IKeyboardBackend +{ + update (): KeyboardState; +} + +export interface Stick +{ + x: number; // -1 → 1 + y: number; // -1 → 1 +} + +export interface Triggers +{ + left: number; // 0 → 1 + right: number; // 0 → 1 +} + +export interface GamepadState +{ + buttons: Record; + leftStick: Stick; + rightStick: Stick; + triggers: Triggers; +} + +export interface IGamepadBackend +{ + /** Polls the current state; returns null if disconnected */ + update (): GamepadState | null; + + /** Optional: release resources (like closing fd on Linux) */ + close?(): void; + + /** Optional: check if the gamepad is still connected */ + isConnected?(): boolean; +} \ No newline at end of file diff --git a/src/bun/api/controls/windows.ts b/src/bun/api/controls/windows.ts new file mode 100644 index 0000000..2621c26 --- /dev/null +++ b/src/bun/api/controls/windows.ts @@ -0,0 +1,117 @@ +import { IGamepadBackend, GamepadState, ButtonName, IKeyboardBackend, KeyboardState, KeyCode } from "./types"; +import { dlopen, FFIType } from "bun:ffi"; + +const xinput = dlopen("xinput1_4.dll", { + XInputGetState: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.u32 }, +}); + +const user32 = dlopen("user32.dll", { + GetAsyncKeyState: { + args: [FFIType.i32], + returns: FFIType.i16, + }, +}); + + +// Virtual key codes +const VK: Record = { + ArrowUp: 0x26, + ArrowDown: 0x28, + ArrowLeft: 0x25, + ArrowRight: 0x27, + KeyW: 0x57, + KeyA: 0x41, + KeyS: 0x53, + KeyD: 0x44, + Enter: 0x0D, + Escape: 0x1B, + Space: 0x20, + End: 0x23, + LeftShift: 0xA0, + RightShift: 0xA1, + LeftControl: 0xA2, + RightControl: 0xA3, + LeftAlt: 0xA4, + RightAlt: 0xA5, +}; + +const ERROR_SUCCESS = 0; + +export class KeyboardWindows implements IKeyboardBackend +{ + private keys: Record = {} as any; + + update (): KeyboardState + { + const next: Record = {} as any; + + // default all keys to false + + // poll keys globally + for (const vkStr in VK) + { + const vk = Number(VK[vkStr as KeyCode]); + const key = vkStr; + + const state = user32.symbols.GetAsyncKeyState(vk); + + if ((state & 0x8000) !== 0) + { + next[key as KeyCode] = true; + } + } + + this.keys = next; + + return { keys: this.keys }; + } +} + +export class GamepadWindows implements IGamepadBackend +{ + private index: number; + private buffer = new ArrayBuffer(16); + private view = new DataView(this.buffer); + private currButtons = 0; + + constructor(index = 0) { this.index = index; } + + update (): GamepadState | null + { + const res = xinput.symbols.XInputGetState(this.index, this.buffer); + if (res !== ERROR_SUCCESS) return null; + + this.prevButtons = this.currButtons; + this.currButtons = this.view.getUint16(4, true); + + const btns: Record = { + A: (this.currButtons & 0x1000) !== 0, + B: (this.currButtons & 0x2000) !== 0, + X: (this.currButtons & 0x4000) !== 0, + Y: (this.currButtons & 0x8000) !== 0, + UP: (this.currButtons & 0x0001) !== 0, + DOWN: (this.currButtons & 0x0002) !== 0, + LEFT: (this.currButtons & 0x0004) !== 0, + RIGHT: (this.currButtons & 0x0008) !== 0, + LB: (this.currButtons & 0x0100) !== 0, + RB: (this.currButtons & 0x0200) !== 0, + START: (this.currButtons & 0x0010) !== 0, + SELECT: (this.currButtons & 0x0020) !== 0, + L3: (this.currButtons & 0x0040) !== 0, + R3: (this.currButtons & 0x0080) !== 0, + }; + + return { + buttons: btns, + leftStick: { x: this.view.getInt16(6, true) / 32767, y: this.view.getInt16(8, true) / 32767 }, + rightStick: { x: this.view.getInt16(10, true) / 32767, y: this.view.getInt16(12, true) / 32767 }, + triggers: { left: this.view.getUint8(14) / 255, right: this.view.getUint8(15) / 255 }, + }; + } + + isConnected () + { + const res = xinput.symbols.XInputGetState(this.index, this.buffer); + return res === ERROR_SUCCESS; + } +} \ No newline at end of file diff --git a/src/bun/api/drives.ts b/src/bun/api/drives.ts index e714038..99452d8 100644 --- a/src/bun/api/drives.ts +++ b/src/bun/api/drives.ts @@ -1,7 +1,7 @@ -import { Drive } from "@/shared/constants"; import si from 'systeminformation'; import fs from 'node:fs'; import os from "node:os"; +import { Drive } from '@simeonradivoev/gameflow-sdk/shared'; async function getAccess (path: string) { diff --git a/src/bun/api/emulatorjs/emulatorjs.ts b/src/bun/api/emulatorjs/emulatorjs.ts index c8018de..d733d34 100644 --- a/src/bun/api/emulatorjs/emulatorjs.ts +++ b/src/bun/api/emulatorjs/emulatorjs.ts @@ -1,4 +1,12 @@ // ES-DE to emulator JS mapping + +import Elysia, { status } from "elysia"; +import z from "zod"; +import path from 'node:path'; +import { config, events, plugins } from "../app"; +import { getLocalGame, updateLocalLastPlayed } from "../games/services/statusService"; +import { SaveFileChange } from "@simeonradivoev/gameflow-sdk/shared"; + // TODO: use the retroarch cores based on ES-DE export const cores: Record = { "atari5200": "atari5200", @@ -43,4 +51,57 @@ export const cores: Record = { "plus4": "plus4", "vic20": "vic20", "dos": "dos" -}; \ No newline at end of file +}; + +export default new Elysia({ prefix: '/emulatorjs' }) + .put('/save', async ({ body: { save, screenshot } }) => + { + await Bun.write(path.join(config.get('downloadPath'), 'saves', "EMULATORJS", save.name), save); + }, { + body: z.object({ + save: z.file(), + screenshot: z.file().optional() + }) + }).get('/load', async ({ query: { filePath } }) => + { + return Bun.file(path.join(config.get('downloadPath'), 'saves', "EMULATORJS", filePath)); + }, { query: z.object({ filePath: z.string() }) }) + .post('/post_play/:source/:id', async ({ params: { source, id }, body: { save } }) => + { + const localGame = await getLocalGame(source, id); + if (!localGame) return status("Not Found"); + + const changedSaveFiles: Record = {}; + if (save) + { + const savesPath = path.join(config.get('downloadPath'), 'saves', "EMULATORJS"); + const saveFile = path.join(savesPath, save.name); + await Bun.write(saveFile, save); + changedSaveFiles.gameflow = { subPath: save.name, cwd: savesPath, shared: false }; + events.emit('notification', { message: "Save Backed Up", type: "success", icon: "save" }); + } + await updateLocalLastPlayed(localGame.id); + await plugins.hooks.games.postPlay.promise({ + source, + id, + saveFolderSlots: { 'emulatorjs': { cwd: path.join(config.get('downloadPath'), "saves", "EMULATORJS") } }, + gameInfo: { platformSlug: localGame?.platform.slug }, + changedSaveFiles: [], + validChangedSaveFiles: changedSaveFiles, + command: { + id: "EMULATORJS", + command: "", + emulator: "EMULATORJS", + valid: true, + metadata: { + romPath: localGame?.path_fs ?? undefined, + emulatorBin: undefined, + emulatorDir: undefined + } + } + }); + }, { + body: z.object({ + save: z.file().optional() + }) + }); \ No newline at end of file diff --git a/src/bun/api/games/collections.ts b/src/bun/api/games/collections.ts new file mode 100644 index 0000000..ae31430 --- /dev/null +++ b/src/bun/api/games/collections.ts @@ -0,0 +1,16 @@ +import Elysia, { status } from "elysia"; +import { plugins } from "../app"; +import { FrontEndCollection } from "@simeonradivoev/gameflow-sdk/shared"; + +export default new Elysia() + .get('/collections', async () => + { + const collections: FrontEndCollection[] = []; + await plugins.hooks.games.fetchCollections.promise({ collections }); + return collections; + }).get('/collection/:source/:id', async ({ params: { source, id } }) => + { + const collection = await plugins.hooks.games.fetchCollection.promise({ source, id }); + if (!collection) return status("Not Found"); + return collection; + }); \ No newline at end of file diff --git a/src/bun/api/games/games.ts b/src/bun/api/games/games.ts index d9fe0ce..d922b36 100644 --- a/src/bun/api/games/games.ts +++ b/src/bun/api/games/games.ts @@ -1,22 +1,29 @@ import Elysia, { status } from "elysia"; -import { activeGame, config, db, events, taskQueue } from "../app"; -import { and, eq, getTableColumns, sql } from "drizzle-orm"; +import { config, db, emulatorsDb, plugins, taskQueue } from "../app"; +import { and, desc, eq, getTableColumns, inArray, like, sql } from "drizzle-orm"; import z from "zod"; import * as schema from "@schema/app"; import fs from "node:fs/promises"; -import { FrontEndGameType, FrontEndGameTypeDetailed, GameListFilterSchema } from "@shared/constants"; -import { getRomApiRomsIdGet, getRomsApiRomsGet } from "@clients/romm"; +import { SERVER_URL } from "@shared/constants"; +import { CommandEntry, DownloadLookupEntry, DownloadsLookupFilterValues, GameListFilterSchema } from '@simeonradivoev/gameflow-sdk/shared'; import { InstallJob } from "../jobs/install-job"; import path from "node:path"; -import { calculateSize, checkInstalled, convertLocalToFrontend, convertRomToFrontend, convertRomToFrontendDetailed, convertStoreToFrontend, convertStoreToFrontendDetailed, getLocalGameMatch } from "./services/utils"; -import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/statusService"; +import { convertLocalToFrontend, getLocalGameMatch, getSourceGameDetailed } from "./services/utils"; +import buildStatusResponse, { customUpdate, fixSource, getValidLaunchCommandsForGame, update, validateGameSource } from "./services/statusService"; import { errorToResponse } from "elysia/adapter/bun/handler"; import { launchCommand } from "./services/launchGameService"; -import { getErrorMessage } from "@/bun/utils"; +import { getErrorMessage, SeededRandom } from "@/bun/utils"; import { defaultFormats, defaultPlugins } from 'jimp'; import { createJimp } from "@jimp/core"; import webp from "@jimp/wasm-webp"; -import { extractStoreGameSourceId, getStoreGame, getStoreGameFromPath, getStoreGameManifest } from "../store/services/gamesService"; +import * as emulatorSchema from '@schema/emulators'; +import { buildStoreFrontendEmulatorSystems, getStoreEmulatorPackage } from "../store/services/gamesService"; +import { host } from "@/bun/utils/host"; +import { LaunchGameJob } from "../jobs/launch-game-job"; +import { cores } from "../emulatorjs/emulatorjs"; +import { findEmulatorPluginIntegration } from "../store/services/emulatorsService"; +import { ImportJob } from "../jobs/import-job"; +import { EmulatorSourceEntryType, EmulatorSystem, FrontEndFilterLists, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailedEmulator, FrontEndGameTypeWithIds, FrontEndId, GameLookup } from "@simeonradivoev/gameflow-sdk/shared"; // A custom jimp that supports webp const Jimp = createJimp({ @@ -26,29 +33,43 @@ const Jimp = createJimp({ async function processImage (img: string | Buffer | ArrayBuffer, { blur, width, height, noBlur }: { blur?: number, width?: number, height?: number; noBlur?: boolean; }) { - if (blur && !noBlur) - { - const jimp = await Jimp.read(img); - if (width) - { - jimp.resize({ w: width, h: height }); - } - if (height) - { - jimp.resize({ w: width, h: height }); - } - if (blur) - { - jimp.blur(blur); - } - return jimp.getBuffer('image/png'); + try + { + if ((blur && !noBlur)) + { + const jimp = await Jimp.read(img); + + if (blur && !noBlur) + { + jimp.blur(blur); + } + + if (width) + { + jimp.resize({ w: width, h: height }); + } else if (height) + { + jimp.resize({ w: width, h: height }); + } + return jimp.getBuffer('image/png'); + } + } catch (e) + { + } if (typeof img === 'string') { - const rommFetch = await fetch(img); - return rommFetch; + const res = await fetch(img); + + return new Response(res.body, { + status: res.status, + headers: { + "Content-Type": res.headers.get("Content-Type") ?? "image/jpeg", + "Cache-Control": "public, max-age=86400", + }, + }); } return img; @@ -74,21 +95,24 @@ export default new Elysia() params: z.object({ id: z.coerce.number() }), query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) }) - .get('/image/:source/*', async ({ params: { source, "*": path }, query }) => + .get('/image/:source/*', async ({ params: { source, "*": path }, query, set }) => { if (source === 'romm') { + set.headers["cross-origin-resource-policy"] = 'cross-origin'; const rommAdress = config.get('rommAddress'); return processImage(`${rommAdress}/${path}`, query); } return status('Not Found'); }, { query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional(), noBlur: z.coerce.boolean().optional() }) }) - .get('/image', async ({ query }) => + .get('/image', async ({ query, set }) => { + set.headers["cross-origin-resource-policy"] = 'cross-origin'; return processImage(query.url, query); }, { query: z.object({ url: z.url(), blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) }) .get('/screenshot/:id', async ({ params: { id }, query, set }) => { + set.headers["cross-origin-resource-policy"] = 'cross-origin'; const screenshot = await db.query.screenshots.findFirst({ where: eq(schema.screenshots.id, id), columns: { content: true, type: true } }); if (screenshot) { @@ -120,10 +144,30 @@ export default new Elysia() }) .get('/games', async ({ query, set }) => { + const games: FrontEndGameType[] = []; + const where: any[] = []; + let localGamesSet: Set | undefined; + if (query.platform_slug) { where.push(eq(schema.platforms.slug, query.platform_slug)); + } else if (query.platform_id && query.platform_source === 'local') + { + where.push(eq(schema.platforms.id, query.platform_id)); + } + else if (query.platform_id && query.platform_source) + { + const platform = await plugins.hooks.games.platformLookup.promise({ source: query.platform_source, id: query.platform_id ? String(query.platform_id) : undefined }); + if (platform) + { + where.push(eq(schema.platforms.slug, platform?.slug)); + } + } + + if (query.search) + { + where.push(like(schema.games.name, query.search)); } if (query.source) @@ -131,189 +175,272 @@ export default new Elysia() where.push(eq(schema.games.source, query.source)); } - const games: FrontEndGameType[] = []; - let localGamesSet: Set | undefined; + const ordering: any[] = []; - if (!query.collection_id) + if (query.orderBy) { - const localGames = await db.select({ - ...getTableColumns(schema.games), - platform: schema.platforms, - screenshotIds: sql`coalesce(json_group_array(${schema.screenshots.id}),json('[]'))`.mapWith(d => JSON.parse(d) as number[]), - }) - .from(schema.games) - .leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)) - .leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id)) - .groupBy(schema.games.id) - .offset(query.offset ?? 0) - .limit(query.limit ?? 50) - .where(and(...where)); + switch (query.orderBy) + { + case 'added': + ordering.push(desc(schema.games.created_at)); + break; + case 'activity': + ordering.push(sql`MAX(COALESCE(${schema.games.created_at}, '1970-01-01'), COALESCE(${schema.games.last_played}, '1970-01-01')) DESC`); + break; + case 'name': + ordering.push(desc(schema.games.name)); + break; + case "release": + ordering.push(sql`json_extract(${schema.games.metadata}, '$.first_release_date') DESC NULLS LAST`); + break; + } + } - localGamesSet = new Set(localGames.filter(g => !!g.source_id && !!g.source).map(g => `${g.source}@${g.source_id}`)); - games.push(...localGames.map(g => + const localGames = await db.select({ + ...getTableColumns(schema.games), + platform: schema.platforms, + screenshotIds: sql`coalesce(json_group_array(${schema.screenshots.id}),json('[]'))`.mapWith(d => JSON.parse(d) as number[]), + }) + .from(schema.games) + .leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)) + .leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id)) + .groupBy(schema.games.id) + .orderBy(...ordering) + .where(and(...where)); + + localGamesSet = new Set( + localGames.filter(g => !!g.source_id && !!g.source).map(g => `${g.source}@${g.source_id}`) + .concat(localGames.filter(g => !!g.igdb_id).map(g => `igdb@${g.igdb_id}`)) + ); + + function localGameExistsPredicate (game: { id: FrontEndId, igdb_id?: number | null, ra_id?: number | null; }) + { + if (localGamesSet?.has(`${game.id.source}@${game.id.id}`)) return true; + if (game.igdb_id && localGamesSet?.has(`igdb@${game.igdb_id}`)) return true; + if (game.ra_id && localGamesSet?.has(`ra@${game.ra_id}`)) return true; + return false; + } + + if (query.collection_id) + { + // Collections are just a remote thing for now. + const remoteGames: FrontEndGameTypeWithIds[] = []; + await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e)); + games.push(...remoteGames.map(g => + { + if (localGameExistsPredicate(g)) + { + return convertLocalToFrontend(localGames.find(g => localGameExistsPredicate({ id: { id: g.source_id ?? '', source: g.source ?? '' }, igdb_id: g.igdb_id, ra_id: g.ra_id }))!); + } + else + { + return g; + } + })); + + } else + { + games.push(...localGames.slice(query.offset, query.limit !== undefined ? ((query.offset ?? 0) + query.limit) : undefined).filter(g => + { + if (query.genres && query.genres.length > 0) + { + if (!g.metadata) return false; + if (!g.metadata.genres) return false; + if (query.genres.some(genre => !g.metadata?.genres?.includes(genre))) return false; + } + + return true; + }).map(g => { return convertLocalToFrontend(g); })); - } - if (((!query.platform_source || query.platform_source === 'romm') || !!query.collection_id) && (!query.source || query.source === 'romm')) - { - const rommGames = await getRomsApiRomsGet({ - query: { - platform_ids: query.platform_id ? [query.platform_id] : undefined, - collection_id: query.collection_id, - limit: query.limit, - offset: query.offset - }, throwOnError: true - }); - games.push(...rommGames.data.items.filter(g => !localGamesSet?.has(`romm@${g.id}`)).map(g => + if (query.localOnly !== true) { - return convertRomToFrontend(g); - })); - } - - if (query.source === 'store') - { - const gamesManifest = await getStoreGameManifest(); - set.headers['x-max-items'] = gamesManifest.filter(g => g.type === 'blob').length; - - const storeGames = await Promise.all(gamesManifest - .slice(query.offset ?? 0, Math.min((query.offset ?? 0) + (query.limit ?? 50), gamesManifest.length)) - .map(async (e) => + const remoteGames: FrontEndGameTypeWithIds[] = []; + const remoteGameSet = new Set(); + await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e)); + games.push(...remoteGames.filter(g => { - const system = path.dirname(e.path); - const id = path.basename(e.path, path.extname(e.path)); - - const localGame = await db.query.games.findFirst({ columns: { id: true }, where: and(eq(schema.games.source, 'store'), eq(schema.games.source_id, `${system}@${id}`)) }); - - if (localGame) + if (localGameExistsPredicate(g)) { - return undefined; + return false; } - const storeGame = await getStoreGameFromPath(e.path); + if (g.igdb_id) + { + const igdbId = `igdb@${g.igdb_id}`; + if (remoteGameSet.has(igdbId)) return false; + remoteGameSet.add(igdbId); + } - return convertStoreToFrontend(system, id, storeGame); + if (g.ra_id) + { + const raId = `ra@${g.ra_id}`; + if (remoteGameSet.has(raId)) return false; + remoteGameSet.add(raId); + } + + return true; })); - games.push(...storeGames.filter(g => g !== undefined)); + } + } + + if (query.orderBy) + { + switch (query.orderBy) + { + case 'added': + games.sort((a, b) => b.updated_at.getTime() - a.updated_at.getTime()); + break; + case 'activity': + games.sort((a, b) => Math.max(b.updated_at.getTime(), b.last_played?.getTime() ?? 0) - Math.max(a.updated_at.getTime(), a.last_played?.getTime() ?? 0)); + break; + case 'name': + games.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); + break; + case "release": + games.sort((a, b) => (b.metadata.first_release_date?.getTime() ?? 0) - (a.metadata.first_release_date?.getTime() ?? 0)); + break; + } + } return { games }; }, { query: GameListFilterSchema, }) - .get('/rom/:source/:id', async ({ params: { id, source } }) => + .get('/games/filters', async ({ query: { source } }) => { - const localGame = await db.query.games.findFirst({ - where: getLocalGameMatch(id, source), - columns: { path_fs: true } + const filterSets: FrontEndFilterSets = { + age_ratings: new Set(), + player_counts: new Set(), + languages: new Set(), + companies: new Set(), + genres: new Set() + }; + + let filter: any = undefined; + if (source) filter = eq(schema.games.source, source); + const local_metadata = await db.query.games.findMany({ columns: { metadata: true }, where: filter }); + + local_metadata.forEach(game => + { + game.metadata.age_ratings?.forEach(r => filterSets.age_ratings.add(r)); + game.metadata.genres?.forEach(r => filterSets.genres.add(r)); + game.metadata.companies?.forEach(r => filterSets.companies.add(r)); + + if (game.metadata.player_count) + filterSets.player_counts.add(game.metadata.player_count); }); - if (!localGame?.path_fs) + await plugins.hooks.games.fetchFilters.promise({ filters: filterSets, source }); + + const filters: FrontEndFilterLists = { + age_ratings: Array.from(filterSets.age_ratings), + player_counts: Array.from(filterSets.player_counts), + languages: Array.from(filterSets.languages), + companies: Array.from(filterSets.companies), + genres: Array.from(filterSets.genres) + }; + + return filters; + }, { + query: z.object({ source: z.string().optional() }) + }) + .get('/rom/:source/:id', async ({ params: { id, source } }) => + { + const filePaths = await plugins.hooks.games.fetchRomFiles.promise({ source, id }); + + if (!filePaths || filePaths.length <= 0) { - return status("Not Found"); + return status("Not Found", "No Valid Roms Found"); } - const downloadPath = config.get('downloadPath'); - const path_fs = path.join(downloadPath, localGame.path_fs); - const stats = await fs.stat(path_fs); - if (stats.isDirectory()) - { - return status("Not Found", "Rom is a folder"); - } + return Bun.file(filePaths[0]); - return Bun.file(path_fs); }, { params: z.object({ source: z.string(), id: z.string() }) }) .get('/game/:source/:id', async ({ params: { source, id } }) => { - async function getLocalGameDetailed (match: any) + const sourceData = await getSourceGameDetailed(source, id); + + if (sourceData) { - const localGame = await db.query.games.findFirst({ - where: match, - with: { - screenshots: { columns: { id: true } }, - platform: { columns: { name: true, slug: true } } - } - }); - if (localGame) + if (sourceData.platform_slug) { - const exists = await checkInstalled(localGame.path_fs); - const fileSize = await calculateSize(localGame.path_fs); - const game: FrontEndGameTypeDetailed = { - path_cover: `/api/romm/game/local/${localGame.id}/cover`, - updated_at: localGame.created_at, - id: { id: String(localGame.id), source: 'local' }, - path_platform_cover: `/api/romm/platform/local/${localGame.platform_id}/cover`, - fs_size_bytes: fileSize ?? null, - paths_screenshots: localGame.screenshots.map(s => `/api/romm/screenshot/${s.id}`), - local: true, - missing: !exists, - platform_display_name: localGame.platform?.name, - summary: localGame.summary, - source: localGame.source, - source_id: localGame.source_id, - path_fs: localGame.path_fs, - last_played: localGame.last_played, - slug: localGame.slug, - name: localGame.name, - platform_id: localGame.platform_id, - platform_slug: localGame.platform.slug - }; - return game; - } - - return undefined; - } - - if (source === 'local') - { - const localGame = await getLocalGameDetailed(eq(schema.games.id, Number(id))); - if (localGame) return localGame; - return status('Not Found'); - } - else - { - const localGame = await getLocalGameDetailed(getLocalGameMatch(id, source)); - if (localGame) return localGame; - - if (source === 'romm') - { - const rom = await getRomApiRomsIdGet({ path: { id: Number(id) } }); - if (rom.data) + const systemMapping = await emulatorsDb.query.systemMappings.findFirst({ where: and(eq(emulatorSchema.systemMappings.sourceSlug, sourceData.platform_slug), eq(emulatorSchema.systemMappings.source, 'romm')) }); + if (systemMapping) { - const romGame = convertRomToFrontendDetailed(rom.data); - return romGame; + const emulatorNames: string[] = []; + await plugins.hooks.emulators.findEmulatorForSystem.promise({ system: systemMapping.system, emulators: emulatorNames }); + + sourceData.emulators = (await Promise.all(emulatorNames.map(async name => + { + if (name === 'EMULATORJS') + { + return { + name: 'EMULATORJS', + validSources: [{ binPath: SERVER_URL(host), type: 'embedded', exists: true }], + logo: 'https://emulatorjs.org/logo/EmulatorJS.png', + systems: await Promise.all(Object.keys(cores).map(async c => + { + const mapping = await emulatorsDb.query.systemMappings.findFirst({ + where (fields, operators) + { + return operators.and(operators.eq(fields.source, "romm"), operators.eq(fields.system, c)); + }, columns: { sourceSlug: true } + }); + const system: EmulatorSystem = { + id: c, + name: c, + iconUrl: `/api/romm/image/romm/assets/platforms/${mapping?.sourceSlug}.svg` + }; + return system; + })), + gameCount: 0, + source: 'local', + integrations: [] + } satisfies FrontEndGameTypeDetailedEmulator; + } + + const foundEmulator = await plugins.hooks.store.fetchEmulator.promise({ id: name }); + + const execPaths: EmulatorSourceEntryType[] = []; + await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: name, sources: execPaths }); + const integrations = findEmulatorPluginIntegration(id, execPaths); + + if (foundEmulator) + { + foundEmulator.validSources = execPaths; + foundEmulator.integrations = integrations; + return foundEmulator; + } + + return { + name: name, + logo: "", + source: 'local', + systems: [], + gameCount: 0, + validSources: execPaths, + integrations: integrations + } satisfies FrontEndGameTypeDetailedEmulator; + }))).filter(e => !!e); } - - return status("Not Found", rom.response); - } - else if (source === 'store') - { - const gameId = extractStoreGameSourceId(id); - const storeGame = await getStoreGame(gameId.system, gameId.id); - if (!storeGame) return status("Not Found"); - return convertStoreToFrontendDetailed(gameId.system, gameId.id, storeGame); } + return sourceData; + } else + { return status("Not Found"); } }, { params: z.object({ source: z.string(), id: z.string() }) }) - .get('/status/:source/:id', async ({ params: { source, id }, set }) => - { - set.headers["content-type"] = 'text/event-stream'; - set.headers["cache-control"] = 'no-cache'; - set.headers['connection'] = 'keep-alive'; - return buildStatusResponse(source, id); - }, { - response: z.any(), - params: z.object({ id: z.string(), source: z.string() }), - query: z.object({ isLocal: z.boolean().optional() }) - }) + .use(buildStatusResponse()) .delete('/game/:source/:id', async ({ params: { source, id } }) => { const deleted = await db.delete(schema.games).where(getLocalGameMatch(id, source)).returning({ path_fs: schema.games.path_fs }); @@ -327,26 +454,83 @@ export default new Elysia() }, { params: z.object({ id: z.string(), source: z.string() }), }) - .post('/game/:source/:id/install', async ({ params: { id, source } }) => + .post('/game/:source/:id/install', async ({ params: { id, source }, body }) => { - if (!taskQueue.hasActive()) + if (!taskQueue.findJob(InstallJob.query({ source, id }), InstallJob)) { - if (source === 'romm' || source === 'store') - { - taskQueue.enqueue(`install-rom-${source}-${id}`, new InstallJob(id, source, id)); - return status(200); - } - - return status('Not Implemented'); + return taskQueue.enqueue(InstallJob.query({ source, id }), new InstallJob(id, source, body)); } else { return status('Not Implemented'); } }, { params: z.object({ id: z.string(), source: z.string() }), + body: z.object({ downloadId: z.string().optional() }).optional(), response: z.any() }) - .post('/game/:source/:id/play', async ({ params: { id, source }, query, set }) => + .delete('/game/:source/:id/install', async ({ params: { id, source } }) => + { + const job = taskQueue.findJob(InstallJob.query({ source, id }), InstallJob); + if (job) + { + job.abort('cancel'); + return status('OK'); + } + return status('Not Found'); + }, { + params: z.object({ id: z.string(), source: z.string() }), + response: z.any() + }) + .get('/game/:source/:id/validate', async ({ params: { id, source } }) => + { + const valid = await validateGameSource(source, id); + return { valid: valid.valid, reason: valid.reason }; + }) + .post('/game/:source/:id/fix_source', async ({ params: { id, source } }) => + { + return fixSource(source, id); + }) + .post('/game/:source/:id/update', async ({ params: { id, source } }) => + { + return update(source, id); + }) + .post('/game/:source/:id/update', async ({ params: { id, source }, body }) => + { + return customUpdate(source, id, body.source, body.id); + }, { body: z.object({ source: z.string(), id: z.string() }) }) + .get('/lookup', async ({ query: { search } }) => + { + const matches = new Map(); + await plugins.hooks.games.gameLookup.promise(matches, { search }); + return { hadMatchers: matches.size > 0, matches: Array.from(matches.values()).flatMap(m => m) }; + }, { + query: z.object({ search: z.string() }) + }) + .get('/lookup/:source/:id', async ({ params: { source, id } }) => + { + const matches = new Map(); + await plugins.hooks.games.gameLookup.promise(matches, { source, id }); + return Array.from(matches.values()).flatMap(m => m); + }) + .get('/game/:source/:id/commands', async ({ params: { id, source }, set }) => + { + const validCommands = await getValidLaunchCommandsForGame(source, id); + if (validCommands instanceof Error) + { + return errorToResponse(validCommands, set); + } + return validCommands as { + commands: CommandEntry[]; + gameId: FrontEndId; + source?: string; + sourceId?: string; + } | undefined; + }, { + response: z.object({ + commands: z.custom().array() + }) + }) + .post('/game/:source/:id/play', async ({ params: { id, source }, body: { command_id }, set }) => { const validCommands = await getValidLaunchCommandsForGame(source, id); if (validCommands) @@ -359,11 +543,11 @@ export default new Elysia() { try { - const validCommand = query.command_id ? validCommands.commands.find(c => c.id === query.command_id) : validCommands.commands[0]; + const validCommand = command_id ? validCommands.commands.find(c => c.id === command_id) : validCommands.commands[0]; if (validCommand) { // launch command waits for the game to exit, we don't want that. - launchCommand(validCommand.command, source, id, validCommands.gameId); + await launchCommand(validCommand, validCommands.gameId, validCommands.source, validCommands.sourceId); return { type: 'application', command: null }; } else { @@ -379,18 +563,15 @@ export default new Elysia() } }, { params: z.object({ id: z.string(), source: z.string() }), - query: z.object({ command_id: z.number().or(z.string()).optional() }), + body: z.object({ command_id: z.number().or(z.string()).optional() }), response: z.object({ type: z.enum(['emulatorjs', 'application']), command: z.string().nullable() }) }) .post("/stop", async ({ }) => { - if (activeGame) + const job = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob); + if (job) { - events.emit('activegameexit', { - source: 'local', id: String(activeGame.gameId), - exitCode: null, - signalCode: null - }); + job.abort('cancel'); } }) .get('/emulatorjs/data/cores/*', async ({ params }) => @@ -398,7 +579,168 @@ export default new Elysia() const res = await fetch(`https://cdn.emulatorjs.org/latest/data/cores/${params['*']}`); return res; }) - .get('/emulatorjs/data/*', async ({ params }) => + .get('/emulatorjs/data/*', async () => { return status("Not Found"); + }) + .get('/recommended/games/emulator/:id', async ({ params: { id } }) => + { + const emulator = await getStoreEmulatorPackage(id); + if (!emulator) return status("Not Found"); + const systems = await buildStoreFrontendEmulatorSystems(emulator); + + const games: FrontEndGameType[] = []; + + let localGamesSet: Set | undefined; + + const localGames = await db.select({ + ...getTableColumns(schema.games), + platform: schema.platforms, + screenshotIds: sql`coalesce(json_group_array(${schema.screenshots.id}),json('[]'))`.mapWith(d => JSON.parse(d) as number[]), + }) + .from(schema.games) + .leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)) + .leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id)) + .groupBy(schema.games.id) + .where(inArray(schema.platforms.slug, systems.map(s => s.id))); + + localGamesSet = new Set(localGames.filter(g => !!g.source_id && !!g.source).map(g => `${g.source}@${g.source_id}`)); + games.push(...localGames.map(g => + { + return convertLocalToFrontend(g); + }).slice(0, 3)); + + const remoteGames: FrontEndGameType[] = []; + await plugins.hooks.games.fetchRecommendedGamesForEmulator.promise({ emulator, systems, games: remoteGames }); + games.push(...remoteGames.filter(g => !localGamesSet?.has(`${g.id.source}@${g.id.id}`))); + + return games; + }) + .get('/recommended/games/game/:source/:id', async ({ params: { source, id } }) => + { + const sourceData = await getSourceGameDetailed(source, id); + if (!sourceData) return status("Not Found"); + + const sourceCompaniesSet = new Set(sourceData.metadata.companies); + const sourceGenresSet = new Set(sourceData.metadata.genres); + + + + const games: (FrontEndGameType & { metadata?: any; })[] = []; + + const localGames = await db.select({ ...getTableColumns(schema.games), platform: schema.platforms }) + .from(schema.games) + .leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)) + .groupBy(schema.games.id); + + const localGamesSourceSet = new Set(localGames.filter(g => g.source).map(g => `${g.source}@${g.source_id}`)); + + games.push(...localGames.map(g => convertLocalToFrontend(g))); + + + + const remoteGames: (FrontEndGameType & { metadata?: any; })[] = []; + plugins.hooks.games.fetchRecommendedGamesForGame.promise({ + game: sourceData, games: remoteGames + }); + + games.push(...remoteGames.filter(g => !localGamesSourceSet.has(`${g.id.source}@${g.id.id}`))); + + const random = new SeededRandom(Math.round(new Date().getTime() / 1000 / 60 / 60)); + + const rankedGames = games.filter(g => + { + if (sourceData.source && g.id.id === sourceData.source_id && g.id.source === sourceData.source) + { + return false; + } + + if (g.id.id === sourceData.id.id && g.id.source === sourceData.id.source) + { + return false; + } + + return true; + }).map(g => + { + let rank = random.next(); + + if (g.platform_slug === sourceData.platform_slug) + rank += 1; + + if (g.id.source === 'local') + rank -= 0.2; + + if (g.metadata) + { + if (g.metadata.companies instanceof Array && g.metadata.companies.some((c: string) => sourceCompaniesSet.has(c))) + { + rank += 1; + } + + if (g.metadata.genres instanceof Array && g.metadata.genres.some((g: string) => sourceGenresSet.has(g))) + { + rank += 1; + } + } + + return { rank: rank, game: g }; + }); + + rankedGames.sort((lhs, rhs) => rhs.rank - lhs.rank); + + return rankedGames.map(g => g.game).slice(0, 10); + }) + .post('/add/custom', async ({ body: { source, id, platformId, gamePath } }) => + { + if (taskQueue.hasActiveOfType(ImportJob)) return status("Conflict", "Import Job Already Running"); + const data = await taskQueue.enqueue(ImportJob.query({ source, id }), new ImportJob(source, id, gamePath, platformId), { + throwOnCancel: true + + }); + return { source: 'local', id: data.localId }; + }, { + body: z.object({ + source: z.string(), + id: z.string(), + gamePath: z.string(), + platformId: z.number() + }) + }).get('/downloads/lookup', async ({ query: { search, page, rows, orderBy, sortDirection, source } }) => + { + const matches = new Map(); + await plugins.hooks.games.downloadsLookup.promise(matches, { search, page, rows, orderBy, sortDirection, source }); + const allValues = Array.from(matches.values()); + return { hadMatchers: matches.size > 0, matches: allValues.flatMap(m => m.items), totalCount: allValues.reduce((p, c) => p + c.count, 0) }; + }, { + query: z.object({ + search: z.string().optional(), + page: z.coerce.number().optional(), + rows: z.coerce.number().optional(), + orderBy: z.string().optional(), + sortDirection: z.literal(["desc", "asc"]).optional(), + source: z.string().optional() + }) + }).get('/download/lookup/:source/:id', async ({ params: { source, id } }) => + { + const match = await plugins.hooks.games.downloadLookup.promise({ source, id }); + if (!match) return status("Not Found"); + return match; + }).get('/download/file/info', async ({ query: { file_url } }) => + { + const response = await fetch(file_url, { method: "HEAD" }); + if (!response.ok) return status('Internal Server Error', response.statusText); + return { size: Number(response.headers.get('content-length')), content_type: response.headers.get('content-type') }; + }, { + query: z.object({ file_url: z.url() }) + }).get('/download/lookup/filters', async () => + { + const filters: DownloadsLookupFilterValues = { + source: [], + orderBy: [] + }; + + await plugins.hooks.games.downloadsLookupFilters.promise({ filters }); + + return filters; }); \ No newline at end of file diff --git a/src/bun/api/games/platforms.ts b/src/bun/api/games/platforms.ts index d8a1e9c..10aaf42 100644 --- a/src/bun/api/games/platforms.ts +++ b/src/bun/api/games/platforms.ts @@ -1,19 +1,14 @@ import Elysia, { status } from "elysia"; -import { getPlatformApiPlatformsIdGet, getPlatformsApiPlatformsGet, getRomsApiRomsGet } from "@clients/romm"; import z from "zod"; -import { count, eq, getTableColumns } from "drizzle-orm"; -import { db } from "../app"; -import { FrontEndPlatformType } from "@shared/constants"; +import { and, count, eq, getTableColumns, not, notExists, or } from "drizzle-orm"; +import { config, db, plugins } from "../app"; import * as schema from "@schema/app"; -import { CACHE_KEYS, getOrCached } from "../cache"; +import { findPlatform } from "./services/utils"; +import { FrontEndPlatformType } from "@simeonradivoev/gameflow-sdk/shared"; export default new Elysia() .get('/platforms', async () => { - const platforms: FrontEndPlatformType[] = []; - let rommPlatformsSet: Set | undefined; - const rommPlatforms = await getOrCached(CACHE_KEYS.ROM_PLATFORMS, () => getPlatformsApiPlatformsGet({ throwOnError: true }), { expireMs: 60 * 60 * 1000 }).then(d => d.data).catch(e => console.error(e)); - const localPlatforms = await db.select({ ...getTableColumns(schema.platforms), game_count: count(schema.games.id) }) .from(schema.platforms) .leftJoin(schema.games, eq(schema.games.platform_id, schema.platforms.id)) @@ -21,31 +16,31 @@ export default new Elysia() const localPlatformSet = new Set(localPlatforms.filter(p => p.game_count > 0).map(p => p.slug)); - if (rommPlatforms) + const remotePlatforms: FrontEndPlatformType[] = []; + + await plugins.hooks.games.fetchPlatforms.promise({ platforms: remotePlatforms }); + + await Promise.all(remotePlatforms.map(async p => { - const frontEndPlatforms = await Promise.all(rommPlatforms.map(async p => + p.hasLocal = localPlatformSet.has(p.slug); + + if (p.paths_screenshots.length <= 0) { - const game = await getRomsApiRomsGet({ query: { platform_ids: [p.id] } }); - const platform: FrontEndPlatformType = { - slug: p.slug, - name: p.display_name, - family_name: p.family_name, - path_cover: `/api/romm/image/romm/assets/platforms/${p.slug}.svg`, - game_count: p.rom_count, - updated_at: new Date(p.updated_at), - id: { source: 'romm', id: String(p.id) }, - hasLocal: localPlatformSet.has(p.slug), - paths_screenshots: game.data?.items[0]?.merged_screenshots.map(s => `/api/romm/image/romm/${s}`) ?? [] - }; + const localScreenshots = await db.select({ id: schema.screenshots.id }).from(schema.games).leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)).where(eq(schema.platforms.slug, p.slug)).leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id)).limit(1); - return platform; - })); + if (localScreenshots) + p.paths_screenshots.push(...localScreenshots.map(s => `/api/romm/screenshot/${s.id}`)); + } - rommPlatformsSet = new Set(rommPlatforms.map(p => p.slug)); - platforms.push(...frontEndPlatforms); - } + const localGames = await db.select({ id: schema.games.id, source: schema.games.source, souceId: schema.games.source_id }).from(schema.games).leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)).where(and(eq(schema.platforms.slug, p.slug), not(eq(schema.games.source, 'romm')))).groupBy(schema.games.id); + p.game_count += localGames.length; + })); - platforms.push(...await Promise.all(localPlatforms.filter(p => !rommPlatformsSet?.has(p.slug)).map(async p => + const platformSlugSet = new Set(remotePlatforms.map(p => p.slug)); + + const platforms: FrontEndPlatformType[] = []; + platforms.push(...remotePlatforms); + platforms.push(...await Promise.all(localPlatforms.filter(p => !platformSlugSet?.has(p.slug)).map(async p => { const game = await db.query.games.findFirst({ where: eq(schema.games.platform_id, p.id) }); let screenshots: { id: number; }[] = []; @@ -73,31 +68,9 @@ export default new Elysia() return { platforms }; }).get('/platforms/:source/:id', async ({ params: { source, id } }) => { - if (source === 'romm') + if (source === 'local') { - const { data: rommPlatform, response } = await getPlatformApiPlatformsIdGet({ path: { id } }); - if (rommPlatform) - { - const platform: FrontEndPlatformType = { - slug: rommPlatform.slug, - name: rommPlatform.display_name, - family_name: rommPlatform.family_name, - path_cover: `/api/romm/image/romm/assets/platforms/${rommPlatform.slug}.svg`, - game_count: rommPlatform.rom_count, - updated_at: new Date(rommPlatform.updated_at), - id: { source: 'romm', id: String(rommPlatform.id) }, - paths_screenshots: [], - hasLocal: false - }; - - return platform; - } - - return status("Not Found", response); - } - else if (source === 'local') - { - const localPlatform = await db.query.platforms.findFirst({ where: eq(schema.platforms.id, id) }); + const localPlatform = await db.query.platforms.findFirst({ where: eq(schema.platforms.id, Number(id)) }); if (localPlatform) { const platform: FrontEndPlatformType = { @@ -116,11 +89,18 @@ export default new Elysia() } return status("Not Found"); + } else + { + const remotePlatform = await plugins.hooks.games.fetchPlatform.promise({ source, id }); + if (!remotePlatform) return status("Not Found"); + const local = await db.query.platforms.findFirst({ where: or(eq(schema.platforms.slug, remotePlatform?.slug), eq(schema.platforms.name, remotePlatform?.name)) }); + return { ...remotePlatform, hasLocal: !!local }; } - - return status("Not Implemented"); - }, { params: z.object({ source: z.string(), id: z.coerce.number() }) }).get('/platform/local/:id/cover', async ({ params: { id }, set }) => + }, { params: z.object({ source: z.string(), id: z.string() }) }) + .get('/platform/local/:id/cover', async ({ params: { id }, set }) => { + set.headers["cross-origin-resource-policy"] = 'cross-origin'; + const coverBlob = await db.query.platforms.findFirst({ columns: { cover: true, cover_type: true @@ -136,4 +116,70 @@ export default new Elysia() set.headers["content-type"] = coverBlob.cover_type; } return status(200, coverBlob.cover); - }, { response: { 200: z.instanceof(Buffer), 404: z.any() }, params: z.object({ id: z.coerce.number() }) }); \ No newline at end of file + }, { response: { 200: z.instanceof(Buffer), 404: z.any() }, params: z.object({ id: z.coerce.number() }) }) + .post('/platform/:source/:id/update', async ({ params: { source, id } }) => + { + const where: any[] = []; + if (source === 'local') + { + where.push(eq(schema.platforms.id, Number(id))); + } else + { + const remotePlatform = await plugins.hooks.games.fetchPlatform.promise({ source, id }); + if (remotePlatform) + { + where.push(eq(schema.platforms.slug, remotePlatform.slug)); + } + } + + const localPlatform = await db.query.platforms.findFirst({ + where: or(...where) + + }); + if (!localPlatform) return status("Not Found"); + + const platformLookup = await plugins.hooks.games.platformLookup.promise({ + slug: localPlatform.slug + }); + let platformCover = await fetch(`${config.get('rommAddress') ?? 'https://demo.romm.app'}/assets/platforms/${localPlatform.slug}.svg`); + if (!platformCover.ok && platformLookup?.url_logo) + { + platformCover = await fetch(platformLookup.url_logo); + } + + await db.update(schema.platforms).set({ + name: platformLookup?.name, + cover: Buffer.from(await platformCover.arrayBuffer()), + cover_type: platformCover.headers.get('content-type'), + }).where(eq(schema.platforms.id, localPlatform.id)); + }) + .delete('/platform/local/:id', async ({ params: { id } }) => + { + const deleted = await db.delete(schema.platforms).where(and(eq(schema.platforms.id, Number(id)), + notExists( + db + .select() + .from(schema.games) + .where(eq(schema.games.platform_id, Number(id))) + ))).returning(); + if (deleted.length <= 0) return status("Not Found"); + }) + .get('/platform/lookup/match/:source/:id', async ({ params: { source, id } }) => + { + const platformLookup = await plugins.hooks.games.platformLookup.promise({ source, id }); + if (!platformLookup) return status("Not Found"); + const match = await findPlatform({ + system_slug: platformLookup.slug, + platform: { + source_slug: platformLookup.slug, + source_id: Number(id), + source: source, + name: platformLookup.name + } + }); + return { details: platformLookup, match }; + }, { + detail: { + description: "Find matches of remote platform lookups. Returns the operations for each platform if it were to be imported. If platform locally exists. Will a new local platform be created from say romm. Unknown is returned if no match is found." + } + }); \ No newline at end of file diff --git a/src/bun/api/games/services/launchGameService.ts b/src/bun/api/games/services/launchGameService.ts index bdd0fd2..490850d 100644 --- a/src/bun/api/games/services/launchGameService.ts +++ b/src/bun/api/games/services/launchGameService.ts @@ -1,350 +1,72 @@ import path from 'node:path'; -import { which } from 'bun'; +import { Glob } from 'bun'; import fs from 'node:fs/promises'; -import { existsSync, readFileSync } from 'node:fs'; -import * as schema from '@schema/emulators'; -import * as appSchema from "@schema/app"; -import { eq } from 'drizzle-orm'; -import { activeGame, config, db, emulatorsDb, events, setActiveGame } from '../../app'; -import os from 'node:os'; -import { $ } from 'bun'; -import { spawn } from 'node:child_process'; -import { updateRomUserApiRomsIdPropsPut } from '@/clients/romm'; -import { CommandEntry } from '@/shared/constants'; +import { existsSync } from 'node:fs'; +import { config, taskQueue } from '../../app'; +import { LaunchGameJob } from '../../jobs/launch-game-job'; +import { getStoreEmulatorPackage } from '../../store/services/gamesService'; +import { getOrCachedScoopPackage } from '../../store/services/emulatorsService'; +import { CommandEntry, EmulatorSourceEntryType, FrontEndId } from '@simeonradivoev/gameflow-sdk/shared'; -export const varRegex = /%([^%]+)%/g; - -export async function launchCommand (validCommand: string, source: string, sourceId: string, id: number) +export async function launchCommand (validCommand: CommandEntry, id: FrontEndId, source?: string, sourceId?: string) { - if (activeGame && activeGame.process?.killed === false) + if (taskQueue.hasActiveOfType(LaunchGameJob)) { - throw new Error(`${activeGame.name} currently running`); + throw new Error(`Game currently running`); } - const localGame = await db.query.games.findFirst({ - where: eq(appSchema.games.id, id), columns: { - name: true, - source_id: true, - source: true - } - }); - - await new Promise((resolve, reject) => - { - const game = spawn(validCommand, { - shell: true - }); - game.stdout.on('data', data => console.log(data)); - game.on('close', (code) => - { - events.emit('activegameexit', { source, id: sourceId, exitCode: code, signalCode: null }); - resolve(code); - }); - game.on('error', e => - { - console.error(e); - events.emit('notification', { message: e.message, type: 'error' }); - reject(e); - }); - - setActiveGame({ - process: game, - name: localGame?.name ?? "Unknown", - gameId: id, - command: validCommand - }); - - function updateRommProps (id: number) - { - updateRomUserApiRomsIdPropsPut({ path: { id }, body: { update_last_played: true } }); - events.emit('notification', { message: "Updated Last Played", type: 'success' }); - } - - if (source === 'romm') - { - updateRommProps(Number(sourceId)); - } - else if (localGame?.source === 'romm' && localGame.source_id) - { - updateRommProps(Number(localGame.source_id)); - } - }); - - /* Old spawn lanching, cases issues, needs to be ran as shell - - const cmd = Array.from(validCommand.command.command.matchAll(/(".*?"|[^\s"]+)/g)).map(m => m[0]); - const game = setActiveGame({ - process: Bun.spawn({ - cmd, - env: { - ...process.env - }, - onExit (subprocess, exitCode, signalCode, error) - { - events.emit('activegameexit', { subprocess, exitCode, signalCode, error }); - }, - stdin: "ignore", - stdout: "inherit", - stderr: "inherit", - }), - name: localGame?.name ?? "Unknown", - gameId: validCommand.gameId, - command: validCommand.command.command - }); - - await game.process.exited; - if (game.process.exitCode && game.process.exitCode > 0) - { - return status('Internal Server Error'); - }*/ + taskQueue.enqueue(LaunchGameJob.id, new LaunchGameJob(id, validCommand, source, sourceId)); } -export async function getValidLaunchCommands (data: { - systemSlug: string; - gamePath: string; - customEmulatorConfig: { - get: (id: string) => string | undefined, - has: (id: string) => boolean, - }; -}): Promise +export async function findStoreEmulatorExec (id: string, emulator?: { systempath: string[]; }): Promise { - - const system = await emulatorsDb.query.systems.findFirst({ - with: { commands: true }, - where: eq(schema.systems.name, data.systemSlug) - }); - - if (!system) + const storeEmulatorFolder = path.join(config.get('downloadPath'), 'emulators', id); + const storeExecName = emulator?.systempath.find(name => existsSync(path.join(storeEmulatorFolder, name))); + if (storeExecName) { - throw new Error(`Could not find system '${data.systemSlug}'`); + return { binPath: path.join(storeEmulatorFolder, storeExecName), rootPath: storeEmulatorFolder, exists: true, type: "store" }; } - if (!system.extension || system.extension.length <= 0) + const storeEmulator = await getStoreEmulatorPackage(id); + if (storeEmulator?.downloads) { - throw new Error(`No extensions listed for system '${data.systemSlug}'`); - } - - const downloadPath = config.get('downloadPath'); - const gamePath = path.join(downloadPath, data.gamePath); - - const validFiles: string[] = []; - if (!existsSync(gamePath)) - { - throw new Error(`Provided rom path is missing: '${gamePath}'`); - } - - const gamePathStat = await fs.stat(gamePath); - - const extensionList = system.extension.join(','); - - if (gamePathStat.isDirectory()) - { - for await (const file of fs.glob(path.join(gamePath, `/**/*.{${extensionList}}`))) + const storeExecName = (await Promise.all(storeEmulator.downloads[`${process.platform}:${process.arch}`].map(async dl => { - validFiles.push(file); - } - - if (validFiles.length <= 0) - { - throw new Error(`Could not find valid rom file. Must be a file that ends in '${extensionList}'`); - } - } else - { - if (system.extension.some(e => gamePath.toLocaleLowerCase().endsWith(e.toLocaleLowerCase()))) - { - validFiles.push(gamePath); - } - else - { - throw new Error(`Invalid Rom File. Must be a file that ends in '${extensionList}'`); - } - } - - const formattedCommands = await Promise.all(system.commands.map(async (command, index) => - { - const label = command.label; - let cmd = command.command; - - let emulator: string | undefined = undefined; - let rom = validFiles[0]; - - if (cmd.includes('%ESCAPESPECIALS%')) - rom = rom.replace(/[&()^=;,]/g, ''); - - const staticVars: Record = { - '%ROM%': $.escape(rom), - '%ROMRAW%': validFiles[0], - '%ROMRAWWIN%': $.escape(validFiles[0].replace('/', '\\')), - '%ESPATH%': $.escape(path.dirname(Bun.main)), - '%ROMPATH%': $.escape(gamePath), - '%BASENAME%': $.escape(path.basename(validFiles[0], path.extname(validFiles[0]))), - '%FILENAME%': $.escape(path.basename(validFiles[0])) - }; - - cmd = cmd.replace(/\%INJECT\%=(?[\w\%.\/\\]+)/g, (subscring, injectFile: string) => - { - try + // glob file search causes issues so do manual search + if (await fs.exists(storeEmulatorFolder)) { - const resolvedInjectFile = injectFile.replace(varRegex, (a) => + const glob = (dl as any).pattern ? new Glob((dl as any).pattern) : undefined; + let bin: string | undefined = (dl as any).bin; + if (!bin && dl.type === 'scoop') { - return staticVars[a] ?? a; - }); - if (existsSync(resolvedInjectFile)) - { - const rawContents = readFileSync(resolvedInjectFile, { encoding: 'utf-8' }); - return rawContents.split('\n').map(v => v.replace('\r', '')).join(' '); + const data = await getOrCachedScoopPackage(id, dl.url); + + if (data) + { + bin = data.bin; + } } - return ''; - } catch (error) - { - return ''; + const files = (await fs.readdir(storeEmulatorFolder)) + .filter(f => + { + if (glob && glob.match(f)) return true; + if (bin && f === bin) return true; + }); + + return files.map(f => path.join(storeEmulatorFolder, f)); } - }); + return []; - const matches = Array.from(cmd.matchAll(varRegex)); - const varList = await Promise.all(matches.map(async ([value]) => + }))).flatMap(f => f); + + if (storeExecName.length > 0) { - if (value.startsWith("%EMULATOR_")) - { - const emulatorName = value.substring("%EMULATOR_".length, value.length - 1); - let exec = await findExecByName(emulatorName); - if (data.customEmulatorConfig.has(emulatorName)) - { - exec = { path: data.customEmulatorConfig.get(emulatorName)!, type: 'custom' }; - } - - emulator = emulatorName; - return [[value, exec ? exec : undefined], ['%EMUDIR%', exec ? $.escape(path.dirname(exec.path)) : undefined]]; - } - - const key = value[0].substring(1, value.length - 1); - return [[value, process.env[key]]]; - })); - - const vars = { ...Object.fromEntries(varList.flatMap(l => l)), ...staticVars }; - vars['%ESCAPESPECIALS%'] = ""; - vars['%HIDEWINDOW%'] = ''; - - // missing variable - const invalid = Object.entries(vars).find(c => c[1] === undefined); - - const formattedCommand = cmd.replace(varRegex, (s) => vars[s] ?? '').trim(); - - return { - id: index, - label: label ?? undefined, - command: formattedCommand, - valid: !invalid, emulator - } satisfies CommandEntry; - })); - - return formattedCommands.filter(c => !!c); -} - -export async function findExecByName (emulatorName: string) -{ - const emulator = await emulatorsDb.query.emulators.findFirst({ where: eq(schema.emulators.name, emulatorName) }); - if (!emulator) - { - throw new Error(`Could not find emulator ${emulatorName}`); - } - return findExec(emulator); -} - -export async function findExec (emulator: { winregistrypath: string[], systempath: string[], staticpath: string[]; }) -{ - if (os.platform() === 'win32') - { - const regValues = emulator.winregistrypath; - if (regValues.length > 0) - { - for (const node of regValues) - { - const registryValue = await readRegistryValue(node); - if (registryValue) - { - return { path: registryValue, type: 'registry' }; - } - } - + return { binPath: storeExecName[0], rootPath: storeEmulatorFolder, exists: true, type: 'store' }; } } - const systempaths = emulator.systempath; - if (systempaths.length > 0) - { - const systemPath = await resolveSystemPath(systempaths); - if (systemPath) - { - return { path: systemPath, type: 'system' }; - } - } - const staticPaths = emulator.staticpath; - if (staticPaths.length > 0) - { - const staticPath = await resolveStaticPath(staticPaths); - if (staticPath) - { - return { path: staticPath, type: 'static' }; - } - } + return undefined; } -async function readRegistryValue (text: string) -{ - const params = text.split('|'); - const key = path.dirname(params[0]); - const value = path.basename(params[0]); - const bin = params.length > 1 ? params[1] : undefined; - - const proc = Bun.spawn({ - cmd: ["reg", "QUERY", key, "/v", value], - stdout: "pipe", - stderr: "pipe", - }); - - const output = await new Response(proc.stdout).text(); - await proc.exited; - - if (!output.includes(value)) return null; - - const lines = output.split("\n"); - for (const line of lines) - { - if (line.includes(value)) - { - const parts = line.trim().split(/\s{4,}/); - return bin ? path.join(parts[2], bin) : parts[2]; // registry value - } - } - - return null; -} - -async function resolveStaticPath (entries: string[]) -{ - for (const entry of entries) - { - const resolved = entry.replace("~", os.homedir()); - if (await fs.exists(resolved)) - { - return resolved; - } - } - return null; -} - -async function resolveSystemPath (entries: string[]) -{ - for (const entry of entries) - { - try - { - const found = which(entry); - return found; - } catch { } - } - return null; -} \ No newline at end of file diff --git a/src/bun/api/games/services/statusService.ts b/src/bun/api/games/services/statusService.ts index e022fb8..1eaed5b 100644 --- a/src/bun/api/games/services/statusService.ts +++ b/src/bun/api/games/services/statusService.ts @@ -1,18 +1,19 @@ -import { GameInstallProgress, GameStatusType, RPC_URL, } from "@shared/constants"; -import { activeGame, config, customEmulators, db, events, taskQueue } from "../../app"; -import { getValidLaunchCommands } from "./launchGameService"; -import * as schema from '@schema/app'; +import { config, db, plugins, taskQueue } from "../../app"; import { eq } from "drizzle-orm"; import { getErrorMessage } from "@/bun/utils"; -import { getLocalGameMatch } from "./utils"; -import { getRomApiRomsIdGet } from "@/clients/romm"; +import { checkFiles, getLocalGameMatch, getSourceGameDetailed } from "./utils"; import fs from 'node:fs/promises'; -import { ErrorLike } from "elysia/universal"; -import { getStoreGameFromId } from "../../store/services/gamesService"; -import { cores } from "../../emulatorjs/emulatorjs"; +import Elysia from "elysia"; +import z from "zod"; +import { InstallJob, InstallJobStates } from "../../jobs/install-job"; +import { LaunchGameJob } from "../../jobs/launch-game-job"; +import * as appSchema from "@schema/app"; +import { RPC_URL } from "@/shared/constants"; +import { DownloadSourceSchema } from '@simeonradivoev/gameflow-sdk/shared'; import { host } from "@/bun/utils/host"; +import { CommandEntry, FrontEndId, GameLookup, GameStatusType, LocalDownloadFileEntry } from "@simeonradivoev/gameflow-sdk/shared"; -class CommandSearchError extends Error +export class CommandSearchError extends Error { constructor(status: GameStatusType, message: string) { @@ -23,217 +24,447 @@ class CommandSearchError extends Error export async function getLocalGame (source: string, id: string) { - const localGames = await db.select({ id: schema.games.id, path_fs: schema.games.path_fs, platform_slug: schema.platforms.es_slug }) - .from(schema.games) - .where(getLocalGameMatch(id, source)) - .leftJoin(schema.platforms, eq(schema.games.platform_id, schema.platforms.id)); + const localGame = await db.query.games.findFirst({ + columns: { + id: true, + path_fs: true, + source: true, + source_id: true, + igdb_id: true, + ra_id: true, + main_glob: true + }, + where: getLocalGameMatch(id, source), + with: { + platform: { columns: { slug: true } } + } + }); - if (localGames.length > 0) - { - return localGames[0]; - } - - return undefined; + return localGame; } -export async function getValidLaunchCommandsForGame (source: string, id: string) +/** Update local game's metadata from custom source, not the actual source of the game. Say from metadata providers like IGDB */ +export async function customUpdate (source: string, id: string, destination: string, destinationId: string) +{ + const localGame = await getLocalGame(source, id); + if (!localGame) throw new Error("Could not find Local Game"); + + const matchesMap = new Map(); + await plugins.hooks.games.gameLookup.promise(matchesMap, { source: destination, id: destinationId }); + const matches = matchesMap.values().next().value; + if (!matches || matches?.length <= 0) throw new Error("Could not find destination"); + const match = matches[0]; + + await db.transaction(async (tx) => + { + await tx.delete(appSchema.screenshots).where(eq(appSchema.screenshots.game_id, localGame.id)); + + // pre-fetch screenshots + const screenshots = await Promise.all(match.screenshotUrls.map(s => fetch(s))); + + if (screenshots.length > 0) + { + await tx.insert(appSchema.screenshots).values(await Promise.all(screenshots.map(async (response) => + { + const screenshot: typeof appSchema.screenshots.$inferInsert = { + game_id: localGame.id, + content: Buffer.from(await response.arrayBuffer()), + type: response.headers.get('content-type') + }; + + return screenshot; + }))); + } + + let cover: Buffer | undefined = undefined; + if (match.coverUrl) + { + const coverResponse = await fetch(match.coverUrl); + if (coverResponse.ok) + { + cover = Buffer.from(await coverResponse.arrayBuffer()); + } + } + + await tx.update(appSchema.games).set({ + cover, + metadata: { + age_ratings: match.age_ratings, + genres: match.genres, + player_count: match.player_count ?? undefined, + companies: match.companies, + game_modes: match.game_modes, + average_rating: match.average_rating ?? undefined, + first_release_date: match.first_release_date, + } + }).where(eq(appSchema.games.id, localGame.id)); + }); +} + +export async function update (source: string, id: string) +{ + const localGame = await getLocalGame(source, id); + if (!localGame) throw new Error("Could not find Local Game"); + if (!localGame.source || !localGame.source_id) throw new Error("Game has not source defined"); + const sourceGame = await getSourceGameDetailed(localGame.source, localGame.source_id, { sourceOnly: true }); + if (!sourceGame) throw new Error("Could not find source game"); + + await db.transaction(async (tx) => + { + await tx.delete(appSchema.screenshots).where(eq(appSchema.screenshots.game_id, localGame.id)); + + const paths_screenshots: string[] = [...sourceGame.paths_screenshots.map(s => `${RPC_URL(host)}${s}`)]; + if (paths_screenshots.length <= 0 && sourceGame.igdb_id) + { + const matches = new Map(); + await plugins.hooks.games.gameLookup.promise(matches, { source: 'igdb', id: String(sourceGame.igdb_id) }); + if (matches.size > 0) + { + const firstMatches = matches.values().next().value; + if (firstMatches && firstMatches.length > 0) + { + paths_screenshots.push(...firstMatches[0].screenshotUrls); + } + } + } + + // pre-fetch screenshots + const screenshots = await Promise.all(paths_screenshots.map(s => fetch(s))); + + if (screenshots.length > 0) + { + await tx.insert(appSchema.screenshots).values(await Promise.all(screenshots.map(async (response) => + { + const screenshot: typeof appSchema.screenshots.$inferInsert = { + game_id: localGame.id, + content: Buffer.from(await response.arrayBuffer()), + type: response.headers.get('content-type') + }; + + return screenshot; + }))); + } + + await tx.update(appSchema.games).set({ + metadata: { + age_ratings: sourceGame.metadata.age_ratings, + genres: sourceGame.metadata.genres, + player_count: sourceGame.metadata.player_count ?? undefined, + companies: sourceGame.metadata.companies, + game_modes: sourceGame.metadata.game_modes, + average_rating: sourceGame.metadata.average_rating ?? undefined, + first_release_date: sourceGame.metadata.first_release_date?.getTime() ?? undefined, + } + }).where(eq(appSchema.games.id, localGame.id)); + }); +} + +export async function fixSource (source: string, id: string) +{ + const valid = await validateGameSource(source, id); + if (!valid.valid) + { + if (!valid.localGame) throw new Error("No Local Game"); + if (!valid.localGame.source) throw new Error("No Valid Source"); + + const foundGame = await plugins.hooks.games.searchGame.promise({ + igdb_id: valid.localGame.igdb_id ?? undefined, + ra_id: valid.localGame.ra_id ?? undefined, + source: valid.localGame.source + }); + + if (foundGame) + { + await db.update(appSchema.games).set({ + source: foundGame.id.source, + source_id: foundGame.id.id, + metadata: { + age_ratings: foundGame.metadata.age_ratings, + genres: foundGame.metadata.genres, + player_count: foundGame.metadata.player_count ?? undefined, + companies: foundGame.metadata.companies, + game_modes: foundGame.metadata.game_modes, + average_rating: foundGame.metadata.average_rating ?? undefined, + first_release_date: foundGame.metadata.first_release_date?.getTime() ?? undefined, + } + }).where(eq(appSchema.games.id, valid.localGame.id)); + return true; + } else + { + throw new Error("Could not find Source Game"); + } + } else + { + throw new Error("Game Source Already Valid"); + } +} + +export async function validateGameSource (source: string, id: string): Promise<{ + valid: boolean, + localGame?: { id: number; igdb_id: number | null; ra_id: number | null; source: string | null; }, + reason?: string; +}> +{ + const localGame = await getLocalGame(source, id); + if (!localGame) return { valid: true }; + if (localGame.source && localGame.source_id) + { + const sourceGame = await plugins.hooks.games.fetchGame.promise({ source: localGame.source, id: localGame.source_id }); + if (!sourceGame) return { valid: false, reason: "Source Missing", localGame }; + // Store should be immutable + if (localGame.source !== 'store' && sourceGame.igdb_id !== (localGame.igdb_id ?? undefined) && sourceGame.ra_id !== (localGame.ra_id ?? undefined)) + { + return { valid: false, reason: "Metadata Missmatch", localGame }; + } + } + + return { valid: true, localGame }; +} + +export async function updateLocalLastPlayed (id: number) +{ + await db.update(appSchema.games).set({ last_played: new Date() }).where(eq(appSchema.games.id, Number(id))); +} + +export async function getValidLaunchCommandsForGame (source: string, id: string): Promise<{ commands: CommandEntry[], gameId: FrontEndId, source?: string, sourceId?: string; } | Error | undefined> { const localGame = await getLocalGame(source, id); if (localGame) { - if (localGame.platform_slug) + const commands = await plugins.hooks.games.buildLaunchCommands.promise({ + source: localGame.source, + sourceId: localGame.source_id, + id: { source: 'local', id: String(localGame.id) }, + systemSlug: localGame.platform.slug, + gamePath: localGame.path_fs, + mainGlob: localGame.main_glob, + }); + + if (commands instanceof Error || !commands) return commands; + + const validCommand = commands.find(c => c.valid); + if (validCommand) { - if (localGame.path_fs) - { - - try - { - const commands = await getValidLaunchCommands({ systemSlug: localGame.platform_slug, customEmulatorConfig: customEmulators, gamePath: localGame.path_fs }); - - if (cores[localGame.platform_slug]) - { - const gameUrl = `${RPC_URL(host)}/api/romm/rom/${source}/${id}`; - commands.push({ - id: 'emulatorjs', - label: "Emulator JS", command: `core=${cores[localGame.platform_slug]}&gameUrl=${encodeURIComponent(gameUrl)}`, valid: true, emulator: 'emulatorjs' - }); - } - - const validCommand = commands.find(c => c.valid); - if (validCommand) - { - return { commands: commands.filter(c => c.valid), gameId: localGame.id, source: source, sourceId: id }; - } - else - { - return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${Array.from(new Set(commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator))).join(', ')}`); - } - } catch (error) - { - console.error(error); - return new CommandSearchError('error', getErrorMessage(error)); - } - - } else - { - return new CommandSearchError('error', 'Missing Path'); - } + return { + commands: commands.filter(c => c.valid), + gameId: { id: String(localGame.id), source: 'local' }, + source: localGame.source ?? source, + sourceId: localGame.source_id ? String(localGame.source_id) : id, + }; } else { - return new CommandSearchError('error', 'Missing Platform'); + return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${Array.from(new Set(commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator))).join(', ')}`); } + } else if (source === 'emulator') + { + const commands = await plugins.hooks.games.buildLaunchCommands.promise({ + source, + sourceId: id, + id: { source: source, id: id }, + systemSlug: "", + gamePath: null + }); + if (commands instanceof Error || !commands) return commands; + + const validCommand = commands.find(c => c.valid); + if (validCommand) + { + return { + commands: commands.filter(c => c.valid), + gameId: { id, source } + }; + } + else + { + return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${Array.from(new Set(commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator))).join(', ')}`); + } } return undefined; } -export default async function buildStatusResponse (source: string, id: string) +export default function buildStatusResponse () { - let cleanup: (() => void) | undefined; - let closed = false; - return new Response(new ReadableStream({ - async start (controller) + return new Elysia().ws('/status/:source/:id', { + response: z.discriminatedUnion('status', [ + z.object({ status: z.literal('error'), error: z.unknown() }), + z.object({ status: z.literal('installed'), commands: z.array(z.any()), details: z.string().optional() }), + z.object({ status: z.literal('refresh'), localId: z.number().optional() }), + z.object({ status: z.literal(['queued']) }), + z.object({ status: z.literal('playing'), details: z.string() }), + z.object({ status: z.literal('install'), details: z.string(), sources: DownloadSourceSchema.array() }), + z.object({ status: z.literal('present'), details: z.string() }), + z.object({ status: z.literal(['download', 'extract']), progress: z.number() }), + ]), + message (ws, data) { - const encoder = new TextEncoder(); - - function enqueue (data: GameInstallProgress, event?: 'error' | 'refresh' | 'ping') + if (data === 'cancel') { - if (closed) return; - const evntString = event ? `event: ${event}\n` : ''; - controller.enqueue(encoder.encode(`${evntString}data: ${JSON.stringify(data)}\n\n`)); + const activeTask = taskQueue.findJob(InstallJob.query({ source: ws.data.params.source, id: ws.data.params.id }), InstallJob); + activeTask?.abort('cancel'); } - - await sendLatests(); - - // seems to help with issue of buffers not flushing, keeping the connection open forcefully - const keepAlive = setInterval(() => - { - if (closed) return clearInterval(keepAlive); - try - { - enqueue({}, 'ping'); - } catch - { - closed = true; - clearInterval(keepAlive); - } - }, 15000); - - const sourceId = `${source}-${id}`; + }, + async open (ws) + { + sendLatests().catch(e => ws.send({ status: 'error', error: JSON.stringify(e) })); + const installJobId = InstallJob.query({ source: ws.data.params.source, id: ws.data.params.id }); async function sendLatests () { - if (closed) return; - const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(id, source), columns: { id: true } }); - const activeTask = taskQueue.findJob(`install-rom-${source}-${id}`); + if (ws.readyState > 1) return; + const activeTask = taskQueue.findJob(InstallJob.query({ source: ws.data.params.source, id: ws.data.params.id }), InstallJob); if (activeTask) { - enqueue({ - progress: activeTask.progress, - status: activeTask.state as any - }); + if (activeTask.status === 'queued') + { + ws.send({ status: 'queued' }); + } else + { + ws.send({ status: activeTask.state as InstallJobStates, progress: activeTask.progress }); + } - } else if (activeGame && activeGame.gameId === localGame?.id) + } else if (taskQueue.hasActiveOfType(LaunchGameJob)) { - enqueue({ status: 'playing' as GameStatusType, details: 'Playing' }); + ws.send({ status: 'playing', details: 'Playing' }); } else { - const validCommand = await getValidLaunchCommandsForGame(source, id); + const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(ws.data.params.id, ws.data.params.source) }); + const validCommand = await getValidLaunchCommandsForGame(ws.data.params.source, ws.data.params.id); if (validCommand) { if (validCommand instanceof Error) { - enqueue({ status: validCommand.name as GameStatusType, error: validCommand.message }); + ws.send({ status: 'error', error: validCommand.message }); } else { - enqueue({ status: 'installed', details: validCommand.commands[0].label, commands: validCommand.commands }); + ws.send({ + status: 'installed', + details: validCommand.commands[0].label, + commands: validCommand.commands + }); } - } - else if (source === 'romm') + } else if (!localGame && ws.data.params.source === 'store') { - // TODO: Add Caching - const remoteGame = await getRomApiRomsIdGet({ path: { id: Number(id) } }); - const stats = await fs.statfs(config.get('downloadPath')); - if (remoteGame.data?.fs_size_bytes && remoteGame.data?.fs_size_bytes > stats.bsize * stats.bavail) - { - enqueue({ status: 'error', error: "Not Enough Free Space" }); - } else - { - enqueue({ status: 'install', details: 'Install' }); - } - - } else if (source === 'store') - { - const storeGame = await getStoreGameFromId(id); + const downloads = await plugins.hooks.games.fetchDownloads.promise({ source: ws.data.params.source, id: ws.data.params.id }); + const sources = downloads?.map(d => ({ id: d.id, name: d.id })) ?? []; + /*const storeGame = await getStoreGame(ws.data.params.id); const fileResponse = await fetch(storeGame.file, { method: 'HEAD' }); const size = Number(fileResponse.headers.get('content-length')); const stats = await fs.statfs(config.get('downloadPath')); if (size > stats.bsize * stats.bavail) { - enqueue({ status: 'error', error: "Not Enough Free Space" }); + ws.send({ status: 'error', error: "Not Enough Free Space" }); } else { - enqueue({ status: 'install', details: 'Install' }); + ws.send({ status: 'install', details: 'Install' }); + }*/ + + ws.send({ status: 'install', details: 'Install', sources }); + } else if (!localGame) + { + const files = await plugins.hooks.games.fetchDownloads.promise({ + source: ws.data.params.source, + id: ws.data.params.id + }); + const sources = files?.map(d => ({ id: d.id, name: d.id })) ?? []; + + let filesChecked: LocalDownloadFileEntry[] | undefined; + + if (files && files.length) + { + filesChecked = await checkFiles(files[0].files, !!files[0].extract_path); } + + if (filesChecked && !filesChecked.some(f => f.exists === false || f.matches === false)) + { + ws.send({ status: 'present', details: "Files Exist On Disk, Import" }); + } else + { + const size = filesChecked?.filter(f => f.exists !== true || f.matches !== true).reduce((p, f) => p += f.size ?? 0, 0); + const stats = await fs.statfs(config.get('downloadPath')); + if (size && size > stats.bsize * stats.bavail) + { + ws.send({ status: 'error', error: "Not Enough Free Space" }); + } else if (filesChecked?.some(f => f.exists === true && f.matches === false)) + { + ws.send({ status: 'install', details: 'Some Files Present, Install', sources }); + } + else + { + ws.send({ status: 'install', details: 'Install', sources }); + } + } + } else + { + ws.send({ status: 'error', error: "No Way To Launch" }); } } } const dispose: Function[] = []; - const handleActiveExit = async (data: { error?: ErrorLike; }) => + const handleActiveExit = async (data: { error?: unknown; }) => { if (data.error) { - enqueue({ + ws.send({ status: 'error', error: data.error - }, 'error'); + }); } await sendLatests(); }; - events.on('activegameexit', handleActiveExit); - dispose.push(() => events.off('activegameexit', handleActiveExit)); - dispose.push(taskQueue.on('progress', ({ id, progress, state }) => + dispose.push(taskQueue.on('progress', (data) => { - if (id.endsWith(sourceId)) + if (data.id === installJobId) { - enqueue({ progress, status: state as any }); + ws.send({ status: data.job.state as InstallJobStates, progress: data.progress }); } })); - dispose.push(taskQueue.on('completed', ({ id }) => + dispose.push(taskQueue.on('queued', (data) => { - if (id.endsWith(sourceId)) + if (data.id === installJobId) { - enqueue({}, 'refresh'); + ws.send({ status: 'queued' }); } })); - dispose.push(taskQueue.on('error', ({ id, error }) => + dispose.push(taskQueue.on('ended', (data) => { - if (id.endsWith(sourceId)) + if (data.id === installJobId) { - enqueue({ + ws.send({ status: 'refresh', localId: (data.job.job as InstallJob).localGameId }); + } else if (data.job.job instanceof LaunchGameJob) + { + handleActiveExit({}); + } + })); + dispose.push(taskQueue.on('error', (data) => + { + if (data.id === installJobId) + { + ws.send({ status: 'error', - error: getErrorMessage(error) - }, 'error'); + error: getErrorMessage(data.error) + }); + } else if (data.job.job instanceof LaunchGameJob) + { + handleActiveExit({ error: data.error }); } })); - cleanup = () => + (ws.data as any).cleanup = () => { - closed = true; dispose.forEach(f => f()); }; }, - cancel (reason) + close (ws, code, reason) { - cleanup?.(); - cleanup = undefined; + (ws.data as any).cleanup?.(); }, - })); + }); } \ No newline at end of file diff --git a/src/bun/api/games/services/utils.ts b/src/bun/api/games/services/utils.ts index 66e4944..9bef2f4 100644 --- a/src/bun/api/games/services/utils.ts +++ b/src/bun/api/games/services/utils.ts @@ -1,23 +1,32 @@ import getFolderSize from "get-folder-size"; import fs from "node:fs/promises"; import path from "node:path"; -import { config, db, emulatorsDb } from "../../app"; -import { and, eq } from "drizzle-orm"; +import { config, db, emulatorsDb, plugins } from "../../app"; +import { and, eq, or } from "drizzle-orm"; import * as schema from "@schema/app"; -import { FrontEndGameType, FrontEndGameTypeDetailed, StoreGameType } from "@shared/constants"; -import { DetailedRomSchema, SimpleRomSchema } from "@clients/romm"; +import { RPC_URL } from "@shared/constants"; +import { hashFile } from "@/bun/utils"; +import { host } from "@/bun/utils/host"; import * as emulatorSchema from "@schema/emulators"; +import { DownloadFileEntry, FrontEndGameType, FrontEndGameTypeDetailed, GameLookup, LocalDownloadFileEntry, LocalGameMetadata, ProgressStats } from "@simeonradivoev/gameflow-sdk/shared"; export async function calculateSize (installPath: string | null) { if (!installPath) return null; - return (await getFolderSize(path.join(config.get('downloadPath'), installPath))).size; + const finalPath = path.isAbsolute(installPath) ? installPath : path.join(config.get('downloadPath'), installPath); + return (await getFolderSize(finalPath)).size; } export async function checkInstalled (installPath: string | null) { if (!installPath) return false; - return fs.exists(path.join(config.get('downloadPath'), installPath)); + const finalPath = path.isAbsolute(installPath) ? installPath : path.join(config.get('downloadPath'), installPath); + return fs.exists(finalPath); +} + +export function getScreenshotLocalGameMatch (id: string, source: string) +{ + return source !== 'local' ? and(eq(schema.games.source_id, id), eq(schema.games.source, source)) : eq(schema.games.id, Number(id)); } export function getLocalGameMatch (id: string, source: string) @@ -25,38 +34,16 @@ export function getLocalGameMatch (id: string, source: string) return source !== 'local' ? and(eq(schema.games.source_id, id), eq(schema.games.source, source)) : eq(schema.games.id, Number(id)); } -export function convertRomToFrontend (rom: SimpleRomSchema): FrontEndGameType -{ - const game: FrontEndGameType = { - id: { id: String(rom.id), source: 'romm' }, - path_cover: `/api/romm/image/romm${rom.path_cover_large}`, - last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null, - updated_at: new Date(rom.updated_at), - slug: rom.slug, - platform_id: rom.platform_id, - platform_display_name: rom.platform_display_name, - name: rom.name, - path_fs: null, - path_platform_cover: `/api/romm/image/romm/assets/platforms/${rom.platform_slug}.svg`, - source: null, - source_id: null, - paths_screenshots: rom.merged_screenshots.map(s => `/api/romm/image/romm/${s}`), - platform_slug: rom.platform_slug - }; - - return game; -} - export function convertLocalToFrontend (g: typeof schema.games.$inferSelect & { platform?: typeof schema.platforms.$inferSelect | null; screenshotIds?: number[]; }) { const game: FrontEndGameType = { - platform_display_name: g.platform?.name ?? "Local", + platform_display_name: g.platform?.name ?? null, id: { id: String(g.id), source: 'local' }, updated_at: g.created_at, - path_cover: `/api/romm/game/local/${g.id}/cover`, + path_covers: [`/api/romm/game/local/${g.id}/cover`], source_id: g.source_id, source: g.source, path_platform_cover: `/api/romm/platform/local/${g.platform_id}/cover`, @@ -66,22 +53,29 @@ export function convertLocalToFrontend (g: typeof schema.games.$inferSelect & { slug: g.slug, name: g.name, platform_id: g.platform_id, - platform_slug: g.platform?.slug ?? null + platform_slug: g.platform?.slug ?? null, + metadata: { + first_release_date: g.metadata?.first_release_date !== undefined ? new Date(g.metadata?.first_release_date) : null + } }; return game; } -export function convertLocalToFrontendDetailed (g: typeof schema.games.$inferSelect & { - platform?: typeof schema.platforms.$inferSelect | null; +export async function convertLocalToFrontendDetailed (g: typeof schema.games.$inferSelect & { + platform?: { name: string | null, slug: string | null; } | null; screenshotIds?: number[]; }) { + + const exists = await checkInstalled(g.path_fs); + const fileSize = await calculateSize(g.path_fs); + const game: FrontEndGameTypeDetailed = { platform_display_name: g.platform?.name ?? "Local", id: { id: String(g.id), source: 'local' }, updated_at: g.created_at, - path_cover: `/api/romm/game/local/${g.id}/cover`, + path_covers: [`/api/romm/game/local/${g.id}/cover`], source_id: g.source_id, source: g.source, path_platform_cover: `/api/romm/platform/local/${g.platform_id}/cover`, @@ -93,94 +87,420 @@ export function convertLocalToFrontendDetailed (g: typeof schema.games.$inferSel platform_id: g.platform_id, platform_slug: g.platform?.slug ?? null, summary: g.summary, - fs_size_bytes: 0, - missing: false, - local: true + fs_size_bytes: fileSize, + missing: !exists, + local: true, + ra_id: g.ra_id, + version: g.version, + version_source: g.version_source, + version_system: g.version_system, + igdb_id: g.igdb_id, + metadata: { + genres: g.metadata.genres ?? [], + companies: g.metadata.companies ?? [], + game_modes: g.metadata.game_modes ?? [], + age_ratings: g.metadata.age_ratings ?? [], + player_count: g.metadata.player_count ?? null, + average_rating: g.metadata.average_rating ?? null, + first_release_date: g.metadata.first_release_date ? new Date(g.metadata.first_release_date) : null + } }; return game; } -export async function convertStoreToFrontend (system: string, id: string, storeGame: StoreGameType): Promise +export async function getLocalGameDetailed (match: any) { - let size: number | null = null; - try - { - const fileResponse = await fetch(storeGame.file, { method: 'HEAD' }); - size = Number(fileResponse.headers.get('content-length')); - } catch (error) - { - console.error(error); - } - const rommSystem = await emulatorsDb.query.systemMappings.findFirst({ - where: and(eq(emulatorSchema.systemMappings.system, system), eq(emulatorSchema.systemMappings.source, 'romm')) + const localGame = await db.query.games.findFirst({ + where: match, + with: { + screenshots: { columns: { id: true } }, + platform: { columns: { name: true, slug: true } } + } }); - const platformDef = await emulatorsDb.query.systems.findFirst({ - where: eq(emulatorSchema.systems.name, system), - columns: { fullname: true } - }); - - const gameId = `${system}@${id}`; - - const game: FrontEndGameType = { - platform_display_name: platformDef?.fullname ?? system, - path_platform_cover: `/api/romm/image/romm/assets/platforms/${rommSystem?.sourceSlug ?? system}.svg`, - id: { source: 'store', id: gameId }, - source: null, - source_id: null, - path_fs: null, - path_cover: `/api/romm/image?url=${encodeURIComponent(storeGame.pictures.titlescreens?.[0])}`, - last_played: null, - updated_at: new Date(), - slug: null, - name: storeGame.title, - platform_id: null, - platform_slug: system, - paths_screenshots: storeGame.pictures.screenshots?.map((s: string) => `/api/romm/image?url=${encodeURIComponent(s)}`) ?? [] - }; - - return game; -} - -export async function convertStoreToFrontendDetailed (system: string, id: string, storeGame: StoreGameType): Promise -{ - let size: number | null = null; - try + if (localGame) { - const fileResponse = await fetch(storeGame.file, { method: 'HEAD' }); - size = Number(fileResponse.headers.get('content-length')); - } catch (error) - { - console.error(error); + return convertLocalToFrontendDetailed({ ...localGame, screenshotIds: localGame.screenshots.map(s => s.id) }); } - const detailed: FrontEndGameTypeDetailed = { - ...await convertStoreToFrontend(system, id, storeGame), - summary: storeGame.description, - fs_size_bytes: size, - missing: false, - local: false, - }; - - return detailed; + return undefined; } -export function convertRomToFrontendDetailed (rom: DetailedRomSchema) +export async function getSourceGameDetailed (source: string, id: string, options?: { sourceOnly?: boolean; }) { - const detailed: FrontEndGameTypeDetailed = { - ...convertRomToFrontend(rom), - summary: rom.summary, - fs_size_bytes: rom.fs_size_bytes, - local: false, - missing: rom.missing_from_fs - }; - if (rom.merged_ra_metadata?.achievements) + if (source === 'local') { - detailed.achievements = { - unlocked: rom.merged_ra_metadata.achievements?.map(a => a.num_awarded).length, - total: rom.merged_ra_metadata.achievements.length + const localGame = await getLocalGameDetailed(eq(schema.games.id, Number(id))); + if (localGame) return localGame; + return undefined; + } + else + { + const localGame = await getLocalGameDetailed(getLocalGameMatch(id, source)); + + const remoteGame = await plugins.hooks.games.fetchGame.promise({ source, id, localGame }); + if (localGame && options?.sourceOnly !== true) + { + return localGame; + } + + return remoteGame; + } +} + +export async function checkFiles (files: DownloadFileEntry[], isArchive: boolean): Promise +{ + return Promise.all(files.map(async f => + { + // file is either zip or doesn't support sha checking + if (!f.sha1 || isArchive) return { ...f, exists: false, matches: false } satisfies LocalDownloadFileEntry; + const localPath = path.join(config.get('downloadPath'), f.file_path, f.file_name); + if (await fs.exists(localPath)) + { + if (f.size && f.size !== (await fs.stat(localPath)).size) + { + return { ...f, exists: true, matches: false } satisfies LocalDownloadFileEntry; + } + + const existingHash = await hashFile(localPath, 'sha1'); + if (existingHash === f.sha1) + { + return { ...f, exists: true, matches: true } satisfies LocalDownloadFileEntry; + } else + { + return { ...f, exists: true, matches: false } satisfies LocalDownloadFileEntry; + } + } + return { ...f, exists: false, matches: false } satisfies LocalDownloadFileEntry; + })); +} + +export async function findPlatform (info: { + system_slug: string; platform: { + igdb_id?: number; + igdb_slug?: string; + ra_id?: number; + moby_id?: number; + source: string; + source_id?: number; + source_slug?: string; + family_name?: string; + name?: string; + } | undefined; +}): + Promise<{ + type: string | null; + slug?: string | null; + name?: string | null; + family_name?: string | null; + es_slug?: string | null; + coverUrl?: string | null; + }> +{ + // Search for existing platform + const platformSearch = [eq(schema.platforms.slug, info.system_slug)]; + const esPlatformSearch = [eq(emulatorSchema.systemMappings.system, info.system_slug)]; + + if (info.platform) + { + if (info.platform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, info.platform.igdb_id)); + if (info.platform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, info.platform.igdb_slug)); + if (info.platform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, info.platform.ra_id)); + if (info.platform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, info.platform.moby_id)); + + esPlatformSearch.push(eq(emulatorSchema.systemMappings.source, info.platform.source)); + if (info.platform.source_slug) + { + esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceSlug, info.platform.source_slug)); + } else if (info.platform.source_id) + { + esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceId, info.platform.source_id)); + } else + { + throw new Error("Must Provide at least one source id or slug"); + } + } + + const esPlatform = await emulatorsDb.query.systemMappings.findFirst({ + with: { system: true }, + where: and(...esPlatformSearch) + }); + + if (esPlatform) + platformSearch.push(eq(schema.platforms.es_slug, esPlatform.system.name)); + + let existingPlatform = await db.query.platforms.findFirst({ where: or(...platformSearch) }); + + if (!existingPlatform) + { + // TODO: use something else than the romm demo as CDN + + const platformLookup = await plugins.hooks.games.platformLookup.promise({ + slug: info.platform?.source_slug ?? info.system_slug + }); + let platformCover = await fetch(`${config.get('rommAddress') ?? 'https://demo.romm.app'}/assets/platforms/${info.platform?.source_slug ?? info.system_slug}.svg`, { method: "HEAD" }); + if (!platformCover.ok && platformLookup?.url_logo) + { + platformCover = await fetch(platformLookup.url_logo, { method: "HEAD" }); + } + + if (!esPlatform && !info.platform) + { + // go to unknown platform + existingPlatform = await db.query.platforms.findFirst({ where: eq(schema.platforms.slug, "unknown") }); + + if (existingPlatform) + { + return { + type: "existing", + slug: existingPlatform.slug, + name: existingPlatform.name, + family_name: existingPlatform.family_name, + es_slug: existingPlatform.es_slug, + coverUrl: `${RPC_URL(host)}/api/romm/platform/local/${existingPlatform.id}/cover` + }; + } else + { + return { type: "unknown" }; + } + } else + { + return { + type: "new", + slug: info.platform?.source_slug ?? esPlatform?.system.name ?? '', + name: info.platform?.name ?? esPlatform?.system.fullname ?? '', + family_name: info.platform?.family_name, + es_slug: esPlatform?.system.name ?? undefined, + coverUrl: platformCover.url + }; + } + + } else + { + return { + type: "existing", + slug: existingPlatform.slug, + name: existingPlatform.name, + family_name: existingPlatform.family_name, + es_slug: existingPlatform.es_slug, + coverUrl: `${RPC_URL(host)}/api/romm/platform/local/${existingPlatform.id}/cover` }; } - return detailed; +} + +export async function createLocalGame (info: { + name: string; + system_slug: string | undefined; + source: string | undefined; + source_id: string | undefined; + slug: string | null | undefined; + path_fs: string | null | undefined; + summary: string | null | undefined; + igdb_id: number | undefined; + ra_id: number | undefined; + main_glob: string | undefined; + cover: Buffer | undefined; + coverType: string | null | undefined; + version: string | undefined; + version_source: string | undefined; + screenshotUrls: string[]; + version_system: string | undefined; + last_played?: Date; + metadata: LocalGameMetadata | undefined, + platform: { + igdb_id?: number; + igdb_slug?: string; + ra_id?: number; + moby_id?: number; + source: string; + source_id?: number; + source_slug?: string; + family_name?: string; + name?: string; + } | undefined; +}) +{ + const id = await db.transaction(async (tx) => + { + // Search for existing platform + const platformSearch = []; + const esPlatformSearch = []; + if (info.system_slug) + { + platformSearch.push(eq(schema.platforms.slug, info.system_slug)); + esPlatformSearch.push(eq(emulatorSchema.systemMappings.system, info.system_slug)); + } + + if (info.platform) + { + if (info.platform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, info.platform.igdb_id)); + if (info.platform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, info.platform.igdb_slug)); + if (info.platform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, info.platform.ra_id)); + if (info.platform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, info.platform.moby_id)); + + esPlatformSearch.push(eq(emulatorSchema.systemMappings.source, info.platform.source)); + if (info.platform.source_slug) + { + esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceSlug, info.platform.source_slug)); + } else if (info.platform.source_id) + { + esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceId, info.platform.source_id)); + } else + { + throw new Error("Must Provide at least one source id or slug"); + } + } + + const esPlatform = await emulatorsDb.query.systemMappings.findFirst({ + with: { system: true }, + where: and(...esPlatformSearch) + }); + + if (esPlatform) + platformSearch.push(eq(schema.platforms.es_slug, esPlatform.system.name)); + + let existingPlatform = await tx.query.platforms.findFirst({ where: or(...platformSearch) }); + let platformId: number; + if (!existingPlatform) + { + // TODO: use something else than the romm demo as CDN + + const platformLookup = await plugins.hooks.games.platformLookup.promise({ + slug: info.platform?.source_slug ?? info.system_slug + }); + let platformCover = await fetch(`${config.get('rommAddress') ?? 'https://demo.romm.app'}/assets/platforms/${info.platform?.source_slug ?? info.system_slug}.svg`); + if (!platformCover.ok && platformLookup?.url_logo) + { + platformCover = await fetch(platformLookup.url_logo); + } + + if (!esPlatform && !info.platform) + { + // go to unknown platform + existingPlatform = await tx.query.platforms.findFirst({ where: eq(schema.platforms.slug, "unknown") }); + + if (existingPlatform) + { + platformId = existingPlatform.id; + } else + { + const [{ id }] = await tx.insert(schema.platforms).values({ + slug: 'unknown', + name: "Unknown" + }).returning({ id: schema.platforms.id }); + platformId = id; + } + } else + { + // Create new local platform + const platform: typeof schema.platforms.$inferInsert = { + slug: info.platform?.source_slug ?? esPlatform?.system.name ?? '', + igdb_id: info.platform?.igdb_id, + igdb_slug: info.platform?.igdb_slug, + ra_id: info.platform?.ra_id, + cover: Buffer.from(await platformCover.arrayBuffer()), + cover_type: platformCover.headers.get('content-type'), + name: info.platform?.name ?? esPlatform?.system.fullname ?? '', + family_name: info.platform?.family_name, + es_slug: esPlatform?.system.name ?? undefined, + }; + + // TODO: add ES slug once I have better way to query ES + const [{ id }] = await tx.insert(schema.platforms).values(platform).returning({ id: schema.platforms.id }); + platformId = id; + } + + } else + { + platformId = existingPlatform.id; + } + + // create the rom + const game: typeof schema.games.$inferInsert = { + source_id: info.source_id, + source: info.source, + slug: info.slug, + path_fs: info.path_fs, + last_played: info.last_played, + platform_id: platformId, + igdb_id: info.igdb_id, + ra_id: info.ra_id, + summary: info.summary, + name: info.name, + cover: info.cover, + cover_type: info.coverType, + metadata: info.metadata, + main_glob: info.main_glob, + version: info.version, + version_source: info.version_source, + version_system: info.version_system + }; + + const [{ id }] = await tx.insert(schema.games).values(game).returning({ id: schema.games.id }); + + if (info.screenshotUrls.length <= 0 && info.igdb_id) + { + const matches = new Map(); + await plugins.hooks.games.gameLookup.promise(matches, { source: 'igdb', id: String(info.igdb_id) }); + info.screenshotUrls.push(...(matches.values().next().value?.[0].screenshotUrls ?? [])); + } + + // pre-fetch screenshots + const screenshots = await Promise.all(info.screenshotUrls.map(s => fetch(s))); + + if (screenshots.length > 0) + { + await tx.insert(schema.screenshots).values(await Promise.all(screenshots.map(async (response) => + { + const screenshot: typeof schema.screenshots.$inferInsert = { + game_id: id, + content: Buffer.from(await response.arrayBuffer()), + type: response.headers.get('content-type') + }; + + return screenshot; + }))); + } + + return id; + }); + + return id; +} + +export async function downloadGame (ctx: { + downloads: DownloadFileEntry[], + auth?: string, + id: string, + abortSignal?: AbortSignal, + setProgress?: (progress: number, state: "download" | "extract", info: Partial>) => void, + extract_path?: string; + path_fs?: string; + +}): Promise +{ + const downloadedFiles = await plugins.hooks.downloadFiles.promise({ + id: ctx.id, + auth: ctx.auth, + files: ctx.downloads, + downloadPath: config.get('downloadPath'), + abortSignal: ctx.abortSignal, + updateProgress: (stats) => ctx.setProgress?.(stats.progress, 'download', stats) + }); + + if (!downloadedFiles) + { + return; + } + + const finalFiles = await plugins.hooks.postDownloadFiles.promise({ + files: downloadedFiles.files, + source: downloadedFiles.source, + extract_path: ctx.extract_path, + downloadPath: config.get('downloadPath'), + path_fs: ctx.path_fs + }) ?? downloadedFiles.files; + + return finalFiles; } \ No newline at end of file diff --git a/src/bun/api/jobs/bios-download-job.ts b/src/bun/api/jobs/bios-download-job.ts new file mode 100644 index 0000000..7a4edba --- /dev/null +++ b/src/bun/api/jobs/bios-download-job.ts @@ -0,0 +1,74 @@ +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; +import { config, plugins } from "../app"; +import { simulateProgress } from "@/bun/utils"; +import { Downloader } from "@/bun/utils/downloader"; +import path from 'node:path'; +import { ensureDir } from "fs-extra"; +import { buildStoreFrontendEmulatorSystems, getStoreEmulatorPackage } from "../store/services/gamesService"; +import { DownloadJobData } from "@simeonradivoev/gameflow-sdk/shared"; + +interface BiosDownloadJobData extends DownloadJobData +{ + emulator: string; +} + +export class BiosDownloadJob implements IJob +{ + static id = "bios-download-job" as const; + static query = (q: { id: string; }) => `${BiosDownloadJob.id}-${q.id}`; + group: string = "bios-download"; + data: BiosDownloadJobData; + dryRun: boolean; + + constructor(emulator: string, init?: { dryRun?: boolean; }) + { + this.data = { + emulator, + name: "Download Emulator Bios" + }; + this.dryRun = init?.dryRun ?? false; + } + + async start (context: JobContext, BiosDownloadJobData, "download">) + { + const emulator = await getStoreEmulatorPackage(this.data.emulator); + if (!emulator) throw new Error("Could Not Find Emulator"); + this.data.name = `${emulator.name} Bios`; + this.data.preview_url = emulator.logo; + const systems = await buildStoreFrontendEmulatorSystems(emulator); + const biosFolder = path.join(config.get('downloadPath'), "bios", this.data.emulator); + await ensureDir(biosFolder); + const files = await plugins.hooks.emulators.fetchBiosDownload.promise({ emulator: this.data.emulator, systems, biosFolder }); + + if (!files) throw new Error("Could not find source to download from"); + + if (this.dryRun) + { + await simulateProgress((p) => context.setProgress(p, 'download'), context.abortSignal); + } else + { + const headers: Record = {}; + if (files.auth) + headers['Authorization'] = files.auth; + + const downloader = new Downloader('bios-download', files.files, biosFolder, { + signal: context.abortSignal, + headers, + onProgress: (stats) => + { + context.setProgress(stats.progress, "download"); + this.data.downloaded = stats.downloaded; + this.data.speed = stats.speed; + this.data.total = stats.total; + }, + }); + + await downloader.start(); + } + } + + exposeData () + { + return this.data; + } +} \ No newline at end of file diff --git a/src/bun/api/jobs/emulator-download-job.ts b/src/bun/api/jobs/emulator-download-job.ts new file mode 100644 index 0000000..de9f538 --- /dev/null +++ b/src/bun/api/jobs/emulator-download-job.ts @@ -0,0 +1,155 @@ +import { DownloadJobData, EmulatorPackageType } from '@simeonradivoev/gameflow-sdk/shared'; +import { getStoreEmulatorPackage } from "../store/services/gamesService"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; +import { config, plugins } from "../app"; +import path from 'node:path'; +import Seven from 'node-7z'; +import fs from "node:fs/promises"; +import { Downloader } from "@/bun/utils/downloader"; +import { ensureDir, move } from "fs-extra"; +import { isArchive, simulateProgress } from "@/bun/utils"; +import { path7za } from "7zip-bin"; +import { getEmulatorDownload, getEmulatorPath } from "../store/services/emulatorsService"; +import { $ } from "bun"; +import { EmulatorSourceEntryType } from "@simeonradivoev/gameflow-sdk/shared"; + +type EmulatorDownloadStates = "download" | "extract"; + +interface EmulatorDownloadJobData extends DownloadJobData +{ + emulator: string; +} + +export class EmulatorDownloadJob implements IJob +{ + static id = "download-emulator" as const; + downloadSource: string; + emulatorPackage?: EmulatorPackageType; + dryRun: boolean; + isUpdate: boolean; + data: EmulatorDownloadJobData = { + name: "Download Emulator", + emulator: "" + }; + + constructor(emulator: string, downloadSource: string, init?: { dryRun?: boolean; isUpdate?: boolean; }) + { + this.data.emulator = emulator; + this.downloadSource = downloadSource; + this.dryRun = init?.dryRun ?? false; + this.isUpdate = init?.isUpdate ?? false; + } + + async start (context: JobContext) + { + this.emulatorPackage = await getStoreEmulatorPackage(this.data.emulator); + if (!this.emulatorPackage) throw new Error("Emulator not found"); + this.data.name = this.emulatorPackage.name; + this.data.preview_url = this.emulatorPackage.logo; + const { url, info } = await getEmulatorDownload(this.emulatorPackage, this.downloadSource); + + const emulatorsFolder = getEmulatorPath(this.data.emulator); + + if (this.dryRun) + { + await simulateProgress(p => context.setProgress(p, "download"), context.abortSignal); + await simulateProgress(p => context.setProgress(p, "extract"), context.abortSignal); + } else + { + const tmpFolder = path.join(config.get("downloadPath"), ".tmp"); + const downloader = new Downloader(this.data.emulator, + [{ url, file_name: path.basename(url.pathname), file_path: this.data.emulator }], + tmpFolder, + { + signal: context.abortSignal, + onProgress: (stats) => + { + context.setProgress(stats.progress, 'download'); + this.data.total = stats.total; + this.data.downloaded = stats.downloaded; + this.data.speed = stats.speed; + }, + }); + + const destinationPaths = await downloader.start(); + context.abortSignal.throwIfAborted(); + if (destinationPaths) + { + const archive = isArchive(destinationPaths[0]); + const isAppImage = destinationPaths[0].endsWith(".AppImage"); + + if (!archive && !isAppImage) + { + throw new Error("Invalid Download Type"); + } + + if (archive) + { + if (destinationPaths[0]) + { + let destinationPath = destinationPaths[0]; + if (destinationPath.endsWith('.tar')) + { + context.setProgress(0, "extract"); + await ensureDir(emulatorsFolder); + await $`tar -xf ${destinationPath} -C ${emulatorsFolder}`; + await fs.rm(destinationPath, { recursive: true }); + } else + { + await new Promise((resolve, reject) => + { + const seven = Seven.extractFull(destinationPath, emulatorsFolder, { $bin: process.env.ZIP7_PATH ?? path7za, $progress: true, noRootDuplication: true }); + seven.on('progress', p => context.setProgress(p.percent, "extract")); + seven.on('error', e => reject(e)); + seven.on('end', () => resolve(true)); + }); + await fs.rm(destinationPath, { recursive: true }); + } + + // check if 1 root folder we need to get rid of + const contents = await fs.readdir(emulatorsFolder); + if (contents.length === 1) + { + const stat = await fs.stat(path.join(emulatorsFolder, contents[0])); + if (stat.isDirectory()) + { + console.log("Found 1 root folder, using that instead"); + const tmpEmulatorsFolder = `${emulatorsFolder} (1)`; + await move(path.join(emulatorsFolder, contents[0]), tmpEmulatorsFolder, { overwrite: true }); + await move(tmpEmulatorsFolder, emulatorsFolder, { overwrite: true }); + } + } + } + } else + { + await ensureDir(emulatorsFolder); + for (const destPath of destinationPaths) + { + await fs.rename(destPath, path.join(emulatorsFolder, path.basename(destPath))); + } + } + + await Bun.write(`${emulatorsFolder}.json`, JSON.stringify(info, null, 3)); + + const execs: EmulatorSourceEntryType[] = []; + await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: this.data.emulator, sources: execs }); + + await plugins.hooks.emulators.emulatorPostInstall.promise({ + emulator: this.data.emulator, + emulatorPackage: this.emulatorPackage, + path: execs.find(e => e.type === 'store')?.binPath ?? emulatorsFolder, + info, + update: this.isUpdate + }); + } + } + + } + + exposeData () + { + return this.data; + } + +} + diff --git a/src/bun/api/jobs/ensure-store.ts b/src/bun/api/jobs/ensure-store.ts new file mode 100644 index 0000000..bce028b --- /dev/null +++ b/src/bun/api/jobs/ensure-store.ts @@ -0,0 +1,64 @@ +import { ensureDir } from "fs-extra"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; +import { getStoreRootFolder } from "../store/services/gamesService"; +import z from "zod"; +import { runBunPackageCommand } from "../plugins/services"; +import { PluginRegistry } from "@/shared/constants"; +import path from "node:path"; +import sdkPkg from '@simeonradivoev/gameflow-sdk/package.json'; +import { IsPluginAllowed } from "@/bun/utils"; + +export default class EnsureStore implements IJob +{ + static id = "update-store" as const; + static dataSchema = z.never(); + packageName: string; + storeVersion: string; + + constructor() + { + this.packageName = process.env.STORE_PACKAGE_NAME ?? "@simeonradivoev/gameflow-store"; + this.storeVersion = process.env.STORE_VERSION ?? "^0.1.0"; + } + + async start (context: JobContext) + { + const storeFolder = getStoreRootFolder(); + await ensureDir(storeFolder); + const storePackageFile = Bun.file(path.join(storeFolder, "package.json")); + if (!await storePackageFile.exists()) + { + await storePackageFile.write(JSON.stringify({ dependencies: {} }, null, 3)); + } + + const storePackage = await Bun.file(path.join(storeFolder, "package.json")).json(); + + if (IsPluginAllowed(sdkPkg.name)) + { + if (!storePackage.dependencies?.[sdkPkg.name] || storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version) + { + let response = await runBunPackageCommand(["add", `${sdkPkg.name}@${sdkPkg.version}`, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + } + + // probably just means we couldn't find a version of the sdk, just install latest + if (storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version) + { + let response = await runBunPackageCommand(["add", '--exact', `${sdkPkg.name}@latest`, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + } + } else + { + console.log("Ignoring SDK package"); + } + + if (process.env.CUSTOM_STORE_PATH) return; + + if (!storePackage.dependencies?.['@simeonradivoev/gameflow-store']) + { + context.setProgress(0.5, "Adding Store"); + let response = await runBunPackageCommand(["add", `${this.packageName}@${this.storeVersion}`, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + } + } +} \ No newline at end of file diff --git a/src/bun/api/jobs/import-job.ts b/src/bun/api/jobs/import-job.ts new file mode 100644 index 0000000..7f080c7 --- /dev/null +++ b/src/bun/api/jobs/import-job.ts @@ -0,0 +1,143 @@ +import { eq, inArray, or } from "drizzle-orm"; +import { db, plugins } from "../app"; +import { createLocalGame, downloadGame } from "../games/services/utils"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; +import * as schema from "@schema/app"; +import { DownloadJobData, GameLookup } from "@simeonradivoev/gameflow-sdk/shared"; +import { isUrl } from "@/shared/utils"; +import { basename } from "node:path"; +import path from 'node:path'; +import { isArchive } from "@/bun/utils"; + +interface ImportJobData extends DownloadJobData +{ + localId: number | null; +} + +export class ImportJob implements IJob +{ + static id = "import-job" as const; + static query = (q: { source: string; id: string; }) => `${ImportJob.id}-${q.source}-${q.id}`; + data: ImportJobData = { + localId: null, + name: "Import Game" + }; + group?: 'import-job'; + gamePath: string; + source: string; + id: string; + platformId: number; + + constructor(source: string, id: string, gamePath: string, platformId: number) + { + this.gamePath = gamePath; + this.source = source; + this.id = id; + this.platformId = platformId; + } + + exposeData () + { + return this.data; + } + + async start (context: JobContext, ImportJobData, string>): Promise + { + const matchesMap = new Map(); + await plugins.hooks.games.gameLookup.promise(matchesMap, { source: this.source, id: this.id }); + const matches = matchesMap.values().next().value; + if (!matches || matches.length <= 0) throw Error("Could not Find Game"); + const match = matches[0]; + this.data.name = match.name; + this.data.preview_url = match.coverUrl; + + let cover: Buffer | undefined = undefined; + let coverType: string | undefined = undefined; + if (match.coverUrl) + { + const coverResponse = await fetch(match.coverUrl); + if (coverResponse.ok) + { + cover = Buffer.from(await coverResponse.arrayBuffer()); + coverType = coverResponse.headers.get('content-type') ?? undefined; + } + } + + const platformMatch = match.platforms.find(p => p.id === this.platformId); + + const finalFiles: string[] = []; + + if (isUrl(this.gamePath)) + { + const archive = isArchive(this.gamePath); + const downloadedFiles = await downloadGame({ + downloads: [{ + file_path: this.id, + file_name: basename(this.gamePath), + url: new URL(this.gamePath) + }], + extract_path: archive ? '.tmp' : undefined, + path_fs: path.join('roms', platformMatch?.slug ?? this.source, this.id), + abortSignal: context.abortSignal, + id: `game-${this.source}-${this.id}`, + setProgress: (progress, state, info) => + { + context.setProgress(progress, state); + this.data.speed = info.speed; + this.data.total = info.total; + this.data.downloaded = info.downloaded; + }, + }); + + if (downloadedFiles) + finalFiles.push(...downloadedFiles); + } else + { + finalFiles.push(this.gamePath); + } + + const localSearchFilters: any[] = []; + if (match.igdb_id) localSearchFilters.push(eq(schema.games.igdb_id, match.igdb_id)); + if (match.slug) localSearchFilters.push(eq(schema.games.slug, match.slug)); + localSearchFilters.push(eq(schema.games.name, match.name)); + localSearchFilters.push(inArray(schema.games.path_fs, finalFiles)); + const existingLocalGame = await db.query.games.findFirst({ where: or(...localSearchFilters) }); + context.abortSignal.throwIfAborted(); + + if (existingLocalGame) throw new Error("Game Already Exists"); + + this.data.localId = await createLocalGame({ + name: match.name, + system_slug: platformMatch?.slug, + source: undefined, + source_id: undefined, + slug: match.slug, + path_fs: finalFiles[0], + summary: match.summary, + igdb_id: match.igdb_id, + ra_id: undefined, + main_glob: undefined, + cover, + coverType, + version: undefined, + version_source: undefined, + screenshotUrls: match.screenshotUrls, + version_system: undefined, + platform: platformMatch ? { + source_slug: platformMatch.slug, + source_id: platformMatch.id, + source: this.source, + name: platformMatch.displayName + } : undefined, + metadata: { + game_modes: match.game_modes, + companies: match.companies, + first_release_date: match.first_release_date ?? undefined, + player_count: match.player_count, + age_ratings: match.age_ratings, + average_rating: match.average_rating, + genres: match.genres, + } + }); + } +} \ No newline at end of file diff --git a/src/bun/api/jobs/install-job.ts b/src/bun/api/jobs/install-job.ts index 89b1912..3d3c867 100644 --- a/src/bun/api/jobs/install-job.ts +++ b/src/bun/api/jobs/install-job.ts @@ -1,333 +1,124 @@ -import { IJob, JobContext } from "../task-queue"; -import { mkdir } from 'node:fs/promises'; -import { and, eq, or } from 'drizzle-orm'; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; import fs from 'node:fs/promises'; -import * as schema from "@schema/app"; -import * as emulatorSchema from "@schema/emulators"; import path from 'node:path'; -import { getPlatformApiPlatformsIdGet, getRomApiRomsIdGet, PlatformSchema } from "@clients/romm"; -import { config, db, emulatorsDb, jar } from "../app"; -import unzip from 'unzip-stream'; -import { Readable, Transform } from "node:stream"; -import { extractStoreGameSourceId, getStoreGameFromId } from "../store/services/gamesService"; -import * as igdb from 'ts-igdb-client'; -import secrets from "../secrets"; +import { config, events, plugins } from "../app"; +import { simulateProgress } from "@/bun/utils"; +import z from "zod"; +import { checkFiles, createLocalGame, downloadGame } from "../games/services/utils"; +import { ensureDir } from "fs-extra"; +import { DownloadInfo, DownloadJobData } from "@simeonradivoev/gameflow-sdk/shared"; interface JobConfig { dryRun?: boolean; dryDownload?: boolean; + downloadId?: string; } -export class InstallJob implements IJob +export type InstallJobStates = 'download' | 'extract'; + +export class InstallJob implements IJob { + static id = "install-job" as const; + static query = (q: { source: string; id: string; }) => `${InstallJob.id}-${q.source}-${q.id}`; + static dataSchema = z.never(); public gameId: string; public source: string; - public sourceId: string; public config?: JobConfig; - static id = "install-job" as const; + // The local game ID of newly created entry, if successful + public localGameId?: number; + public group = InstallJob.id; + public localPath?: string; + data: DownloadJobData = { + name: "Install Game" + }; - constructor(id: string, source: string, sourceId: string, config?: JobConfig) + constructor(id: string, source: string, config?: JobConfig) { this.gameId = id; this.config = config; - this.sourceId = sourceId; this.source = source; } - public async start (cx: JobContext) + public async start (cx: JobContext) { cx.setProgress(0, 'download'); - fs.mkdir(config.get('downloadPath'), { recursive: true }); + await fs.mkdir(config.get('downloadPath'), { recursive: true }); + + const downloadPath = config.get('downloadPath'); + const finalFiles: string[] = []; + let info: DownloadInfo | undefined; if (this.config?.dryRun !== true) { - const downloadPath = config.get('downloadPath'); + const allDownloads = await plugins.hooks.games.fetchDownloads.promise({ source: this.source, id: this.gameId, downloadId: this.config?.downloadId }); + info = allDownloads?.[0]; - let downloadUrl: URL; - let cookie: string = ''; - let screenshotUrls: string[]; - let coverUrl: string; - let rommPlatform: PlatformSchema | undefined; - let slug: string | null; - let path_fs: string | undefined; - let summary: string | null; - let name: string | null; - let last_played: Date | null; - let igdb_id: number | null; - let ra_id: number | null; - let source_id: string; - let system_slug: string; - let extract_path: string; + if (!info) throw new Error(`Could not find downloader for source ${this.source}`); - switch (this.source) + this.data.name = info.name; + this.data.preview_url = info.coverUrl; + + const files = await checkFiles(info.files, !!info.extract_path); + + if (this.config?.dryDownload !== true && files.some(f => !f.exists || !f.matches)) { - case 'romm': - - const rom = (await getRomApiRomsIdGet({ path: { id: Number(this.gameId) }, throwOnError: true })).data; - rommPlatform = (await getPlatformApiPlatformsIdGet({ path: { id: rom.platform_id }, throwOnError: true })).data; - - const rommAddress = config.get('rommAddress'); - coverUrl = `${rommAddress}${rom.path_cover_large}`; - screenshotUrls = rom.merged_screenshots.map(s => `${config.get('rommAddress')}${s}`); - last_played = rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null; - igdb_id = rom.igdb_id; - ra_id = rom.ra_id; - summary = rom.summary; - name = rom.name; - path_fs = path.join(rom.fs_path, rom.fs_name); - source_id = String(rom.id); - slug = rom.slug; - system_slug = rommPlatform.slug; - extract_path = ''; - - downloadUrl = new URL(`${config.get('rommAddress')}/api/roms/download`); - downloadUrl.searchParams.set('rom_ids', String(this.gameId)); - cookie = await jar.getCookieString(config.get('rommAddress') ?? ''); - break; - case 'store': - const game = await getStoreGameFromId(this.gameId); - const gameId = extractStoreGameSourceId(this.gameId); - coverUrl = game.pictures.titlescreens[0]; - screenshotUrls = game.pictures.screenshots; - downloadUrl = new URL(game.file); - slug = this.gameId; - source_id = this.gameId; - name = game.title; - summary = game.description; - system_slug = gameId.system; - extract_path = 'roms', gameId.system; - - break; - default: - throw new Error("Unsupported source"); - } - - if (this.config?.dryDownload !== true) - { - /* - // download files for rom - const downloadUrl = new URL(`${config.get('rommAddress')}/api/roms/download`); - downloadUrl.searchParams.set('rom_ids', String(this.id)); - const downloader = new DownloaderHelper(downloadUrl.href, downloadPath, { - headers: { - cookie: await jar.getCookieString(config.get('rommAddress') ?? '') - }, - fileName: `${this.id}.zip`, - // Romm doesn't support resume download - override: true - }); - - cx.abortSignal.addEventListener('abort', downloader.stop); - - downloader.on('progress.throttled', e => - { - cx.setProgress(e.progress, 'download'); - }); - - downloader.on('error', (e) => - { - cx.abort(e); - }); - const finishPromise = new Promise(resolve => - { - downloader.on("end", ({ filePath }) => resolve(filePath)); - }); - - await downloader.start().catch(err => console.error(err)); - const zipFilePath = await finishPromise; - - cx.setProgress(0, 'extract'); - - const zip = new StreamZip.async({ file: zipFilePath }); - const totalCount = await zip.entriesCount; - let extractCount = 0; - zip.on('extract', async (entry, file) => - { - console.log(`Extracted ${entry.name} to ${file}`); - cx.setProgress(extractCount / totalCount * 100, 'extract'); - extractCount++; - }); - await zip.extract(null, downloadPath); - await zip.close(); - - await fs.rm(zipFilePath);*/ - - cx.setProgress(0, 'download'); - - const res = await fetch(downloadUrl, { - headers: { - cookie: cookie + const downloadedFiles = await downloadGame({ + downloads: files.filter(f => !f.exists || !f.matches), + extract_path: info.extract_path, + path_fs: info.path_fs, + abortSignal: cx.abortSignal, + auth: info.auth, + id: `game-${this.source}-${this.gameId}`, + setProgress: (process, state, info) => + { + cx.setProgress(process, state); + this.data.downloaded = info.downloaded; + this.data.speed = info.speed; + this.data.total = info.total; }, }); - const totalBytes = Number(res.headers.get("content-length")) || 0; - let bytesReceived = 0; - - const progressStream = new Transform({ - transform (chunk, encoding, callback) - { - bytesReceived += chunk.length; - if (totalBytes > 0) - { - const percent = (bytesReceived / totalBytes) * 100; - cx.setProgress(percent, 'download'); - } - this.push(chunk); - callback(); - } - }); - - await new Promise((resolve, reject) => - { - const extract = unzip.Extract({ path: path.join(downloadPath, extract_path), }); - (extract as any).unzipStream.on('entry', (entry: any) => - { - if (!path_fs) - path_fs = path.join(extract_path, entry.path); - }); - Readable.fromWeb(res.body as any).pipe(progressStream) - .pipe(extract) - .on('close', resolve) - .on('error', reject); - }); + if (downloadedFiles) + finalFiles.push(...downloadedFiles); } - if (this.config?.dryDownload === true) + if (this.config?.dryDownload === true && info.extract_path) { - await mkdir(path.join(downloadPath, extract_path), { recursive: true }); + await ensureDir(path.join(downloadPath, info.extract_path)); } - - - const coverResponse = await fetch(coverUrl); + const coverResponse = await fetch(info.coverUrl); const cover = Buffer.from(await coverResponse.arrayBuffer()); - if (cx.abortSignal.aborted) return; - - await db.transaction(async (tx) => - { - // Search for existing platform - const platformSearch = [eq(schema.platforms.slug, system_slug)]; - const esPlatformSearch = [eq(emulatorSchema.systemMappings.system, system_slug)]; - - if (rommPlatform) - { - if (rommPlatform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, rommPlatform.igdb_id)); - if (rommPlatform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, rommPlatform.igdb_slug)); - if (rommPlatform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, rommPlatform.ra_id)); - if (rommPlatform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, rommPlatform.moby_id)); - - esPlatformSearch.push(eq(emulatorSchema.systemMappings.source, 'romm')); - esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceSlug, rommPlatform.slug)); - } - - const esPlatform = await emulatorsDb.query.systemMappings.findFirst({ - with: { system: true }, - where: and(...esPlatformSearch) - }); - - if (esPlatform) - platformSearch.push(eq(schema.platforms.es_slug, esPlatform.system.name)); - - let existingPlatform = await tx.query.platforms.findFirst({ where: or(...platformSearch) }); - let platformId: number; - if (!existingPlatform) - { - // TODO: use something else than the romm demo as CDN - const platformCover = await fetch(`https://demo.romm.app/assets/platforms/${system_slug}.svg`); - - if (!esPlatform && !rommPlatform) - { - // go to unknown platform - existingPlatform = await tx.query.platforms.findFirst({ where: eq(schema.platforms.slug, "unknown") }); - - if (existingPlatform) - { - platformId = existingPlatform.id; - } else - { - const [{ id }] = await tx.insert(schema.platforms).values({ - slug: 'unknown', - name: "Unknown" - }).returning({ id: schema.platforms.id }); - platformId = id; - } - } else - { - // Create new local platform - const platform: typeof schema.platforms.$inferInsert = { - slug: rommPlatform?.slug ?? esPlatform?.system.name ?? '', - igdb_id: rommPlatform?.igdb_id, - igdb_slug: rommPlatform?.igdb_slug, - ra_id: rommPlatform?.ra_id, - cover: Buffer.from(await platformCover.arrayBuffer()), - cover_type: platformCover.headers.get('content-type'), - name: rommPlatform?.name ?? esPlatform?.system.fullname ?? '', - family_name: rommPlatform?.family_name, - es_slug: esPlatform?.system.name ?? undefined - }; - - // TODO: add ES slug once I have better way to query ES - const [{ id }] = await tx.insert(schema.platforms).values(platform).returning({ id: schema.platforms.id }); - platformId = id; - } - - } else - { - platformId = existingPlatform.id; - } - - // create the rom - const game: typeof schema.games.$inferInsert = { - source_id, - source: this.source, - slug, - path_fs, - last_played: last_played, - platform_id: platformId, - igdb_id: igdb_id, - ra_id: ra_id, - summary: summary, - name, - cover, - cover_type: coverResponse.headers.get('content-type') - }; - - const [{ id }] = await tx.insert(schema.games).values(game).returning({ id: schema.games.id }); - - if (screenshotUrls.length <= 0 && process.env.TWITCH_CLIENT_ID) - { - const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); - if (access_token) - { - const client = igdb.igdb(process.env.TWITCH_CLIENT_ID, access_token); - - const { data } = await client.request('artworks').pipe(igdb.fields(['game', 'url']), igdb.where('game', '=', igdb_id)).execute(); - - screenshotUrls.push(...data.filter(s => s.url).map(s => s.url!)); - } - } - - // pre-fetch screenshots - const screenshots = await Promise.all(screenshotUrls.map(s => fetch(s))); - - if (screenshots.length > 0) - { - await tx.insert(schema.screenshots).values(await Promise.all(screenshots.map(async (response) => - { - const screenshot: typeof schema.screenshots.$inferInsert = { - game_id: id, - content: Buffer.from(await response.arrayBuffer()), - type: response.headers.get('content-type') - }; - - return screenshot; - }))); - } + cx.abortSignal.throwIfAborted(); + this.localGameId = await createLocalGame({ + cover, + coverType: coverResponse.headers.get('content-type'), + system_slug: info.system_slug, + source_id: info.source_id, + source: this.source, + slug: info.slug, + path_fs: info.path_fs ?? (info.extract_path ? path.join(downloadPath, info.extract_path) : undefined), + summary: info.summary, + igdb_id: info.igdb_id, + ra_id: info.ra_id, + name: info.name, + main_glob: info.main_glob, + version: info.version, + version_source: info.version_source, + screenshotUrls: info.screenshotUrls, + version_system: info.version_system, + metadata: info.metadata, + platform: info.platform }); - } + if (this.source && this.gameId) await plugins.hooks.games.postInstall.promise({ source: this.source, id: this.gameId, files: finalFiles, info }); + events.emit('notification', { message: `${info.name}: Installed`, type: 'success', duration: 8000 }); + } else + { + await simulateProgress(p => cx.setProgress(p, "download"), cx.abortSignal); + } } } \ No newline at end of file diff --git a/src/bun/api/jobs/jobs.ts b/src/bun/api/jobs/jobs.ts index b4680a6..5471e56 100644 --- a/src/bun/api/jobs/jobs.ts +++ b/src/bun/api/jobs/jobs.ts @@ -1,61 +1,89 @@ import Elysia from "elysia"; -import z, { } from "zod"; +import z, { _ZodType } from "zod"; import { taskQueue } from "../app"; import { LoginJob } from "./login-job"; import TwitchLoginJob from "./twitch-login-job"; -import UpdateStoreJob from "./update-store"; +import EnsureStore from "./ensure-store"; +import { EmulatorDownloadJob } from "./emulator-download-job"; +import { getErrorMessage } from "@/bun/utils"; +import { BaseEvent, IJob } from "@simeonradivoev/gameflow-sdk/task-queue"; +import { LaunchGameJob } from "./launch-game-job"; +import { BiosDownloadJob } from "./bios-download-job"; +import { InstallJob } from "./install-job"; +import ReloadPluginsJob from "./reload-plugins-job"; +import { FrontEndJob } from "@simeonradivoev/gameflow-sdk/shared"; -function registerJob (job: T, path: Path, dataSchema: TS) +function registerJob< + const Path extends string, + Schema, + const States extends string, +> (_job: { + id: Path; + query?: (q: any) => string; +} & (new (...args: any[]) => IJob)) { - return new Elysia().ws(path, { + return new Elysia().ws(_job.id, { body: z.discriminatedUnion('type', [ z.object({ type: z.literal('cancel') }) ]), + query: z.record(z.string(), z.any()), response: z.discriminatedUnion('type', [ z.object({ type: z.literal(['data', 'started', 'progress']), - status: z.string(), + state: z.string().optional(), progress: z.number(), - data: dataSchema + data: z.custom() }), - z.object({ type: z.literal(['completed', 'ended']) }), - z.object({ type: z.literal('error'), error: z.unknown() }) + z.object({ type: z.literal(['completed', 'ended']), data: z.custom() }), + z.object({ type: z.literal('waiting') }), + z.object({ type: z.literal('error'), error: z.string() }) ]), open (ws) { - const job = taskQueue.findJob(path); + const jobId = (_job.query ? _job.query(ws.data.query) : _job.id); + const job = taskQueue.findJob(jobId, _job); if (job) { - ws.send({ type: 'data', status: job.status, progress: job.progress, data: job.job.exposeData?.() }); + ws.send({ type: 'data', state: job.state, progress: job.progress, data: job.job.exposeData?.() as Schema }); + } else + { + ws.send({ type: 'waiting' }); } (ws.data as any).cleanup = [ taskQueue.on('started', ({ id, job }) => { - if (id === path) + if (id === jobId) { - ws.send({ type: 'started', status: job.status, progress: job.progress, data: job.job.exposeData?.() }); + ws.send({ type: 'started', state: job.state, progress: job.progress, data: job.job.exposeData?.() }); } }), taskQueue.on('progress', ({ id, job }) => { - if (id === path) + if (id === jobId) { - ws.send({ type: 'started', status: job.status, progress: job.progress, data: job.job.exposeData?.() }); + ws.send({ type: 'progress', state: job.state, progress: job.progress, data: job.job.exposeData?.() }); } }), - taskQueue.on('completed', ({ id }) => + taskQueue.on('completed', ({ id, job }) => { - if (id === path) + if (id === jobId) { - ws.send({ type: 'completed' }); + ws.send({ type: 'completed', data: job.job.exposeData?.() }); + } + }), + taskQueue.on('ended', ({ id, job }) => + { + if (id === jobId) + { + ws.send({ type: 'ended', data: job.job.exposeData?.() }); } }), taskQueue.on('error', ({ id, error }) => { - if (id === path) + if (id === jobId) { - ws.send({ type: 'error', error: error }); + ws.send({ type: 'error', error: getErrorMessage(error) }); } }) ]; @@ -64,17 +92,100 @@ function registerJob d()); }, - message (ws, message) + message (_, message) { if (message.type === 'cancel') { - taskQueue.findJob(path)?.abort('cancel'); + const jobId = (_job.query ? _job.query(this.query) : _job.id); + taskQueue.findJob(jobId, _job)?.abort('cancel'); } }, }); } export const jobs = new Elysia({ prefix: '/api/jobs' }) - .use(registerJob(LoginJob, LoginJob.id, LoginJob.dataSchema)) - .use(registerJob(TwitchLoginJob, TwitchLoginJob.id, TwitchLoginJob.dataSchema)) - .use(registerJob(UpdateStoreJob, UpdateStoreJob.id, undefined)); + .ws('/list', { + response: z.discriminatedUnion('type', [ + z.object({ type: z.literal("allJobs"), active: z.custom().array(), queued: z.custom().array() }), + z.object({ type: z.literal("started"), job: z.custom() }), + z.object({ type: z.literal("progress"), job: z.custom() }), + z.object({ type: z.literal("queued"), job: z.custom() }), + z.object({ type: z.literal("aborted"), id: z.string() }), + z.object({ type: z.literal("ended"), id: z.string() }), + ]), + body: z.discriminatedUnion('type', [ + z.object({ type: z.literal("cancel"), id: z.string() }) + ]), + message (ws, message) + { + switch (message.type) + { + case "cancel": + taskQueue.cancelJob(message.id); + break; + } + }, + open (ws) + { + ws.send({ + type: 'allJobs', + active: taskQueue.getActiveJobs().map(j => + { + const job: FrontEndJob = { + id: j.id, + data: j.job.exposeData?.(), + progress: j.progress, + state: j.state, + status: j.status + }; + + return job; + }), + queued: taskQueue.getQueuedJobs()?.map(j => + { + const job: FrontEndJob = { + id: j.id, + data: j.job.exposeData?.(), + progress: j.progress, + state: j.state, + status: j.status + }; + + return job; + }) ?? [] + }); + + (ws.data as any).dispose = [taskQueue.on('started', (e: BaseEvent) => + { + ws.send({ type: "started", job: { id: e.id, data: e.job.job.exposeData?.(), progress: e.job.progress, state: e.job.state, status: e.job.status } }); + }), + taskQueue.on('progress', (e: BaseEvent) => + { + ws.send({ type: "progress", job: { id: e.id, data: e.job.job.exposeData?.(), progress: e.job.progress, state: e.job.state, status: e.job.status } }); + }), + taskQueue.on('queued', (e: BaseEvent) => + { + ws.send({ type: "queued", job: { id: e.id, data: e.job.job.exposeData?.(), progress: e.job.progress, state: e.job.state, status: e.job.status } }); + }), + taskQueue.on('abort', (e: BaseEvent) => + { + ws.send({ type: "aborted", id: e.id }); + }), + taskQueue.on('ended', (e: BaseEvent) => + { + ws.send({ type: "ended", id: e.id }); + })]; + }, + close (ws, code, reason) + { + (ws.data as any).dispose.forEach((d: any) => d()); + }, + }) + .use(registerJob(LaunchGameJob)) + .use(registerJob(LoginJob)) + .use(registerJob(TwitchLoginJob)) + .use(registerJob(EnsureStore)) + .use(registerJob(BiosDownloadJob)) + .use(registerJob(InstallJob)) + .use(registerJob(ReloadPluginsJob)) + .use(registerJob(EmulatorDownloadJob)); diff --git a/src/bun/api/jobs/launch-game-job.ts b/src/bun/api/jobs/launch-game-job.ts new file mode 100644 index 0000000..3ce0e83 --- /dev/null +++ b/src/bun/api/jobs/launch-game-job.ts @@ -0,0 +1,277 @@ +import z from "zod"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; +import { ActiveGameSchema, ActiveGameType } from "@simeonradivoev/gameflow-sdk"; +import { config, db, events, plugins } from "../app"; +import * as appSchema from "@schema/app"; +import { eq } from "drizzle-orm"; +import { spawn } from 'node:child_process'; +import { updateLocalLastPlayed } from "../games/services/statusService"; +import { getErrorMessage } from "@/bun/utils"; +import { CommandEntry, FrontEndId, SaveSlots } from "@simeonradivoev/gameflow-sdk/shared"; + +export class LaunchGameJob implements IJob, string> +{ + static id = "launch-game" as const; + static dataSchema = z.nullable(ActiveGameSchema); + group = "launch-game"; + activeGame: ActiveGameType | null; + gameId: FrontEndId; + validCommand: CommandEntry; + gameSource?: string; + gameSourceId?: string; + changedSaveFiles: Map; + saveSlots: SaveSlots = {}; + + constructor(gameId: FrontEndId, validCommand: CommandEntry, source?: string, sourceId?: string) + { + this.gameId = gameId; + this.validCommand = validCommand; + this.gameSource = source; + this.gameSourceId = sourceId; + this.activeGame = null; + this.changedSaveFiles = new Map(); + } + + async postPlay (gameInfo: { platformSlug?: string; }) + { + if (this.gameId.source === 'local') + { + await updateLocalLastPlayed(Number(this.gameId.id)); + } + + const source = this.gameSource ?? this.gameId.source; + const id = this.gameSourceId ?? this.gameId.id; + + await new Promise(async (resolve) => + { + await plugins.hooks.games.postPlay.promise( + { + source, + id, + command: this.validCommand, + changedSaveFiles: Array.from(this.changedSaveFiles.values()), + validChangedSaveFiles: {}, + saveFolderSlots: this.saveSlots, + gameInfo + }).catch(e => + { + console.error(e); + events.emit('notification', { message: getErrorMessage(e), type: 'error' }); + }).then(() => resolve(false)); + const timeoutHandler = () => resolve(false); + setTimeout(timeoutHandler, 5000); + }); + } + + prePlay (setProgress: (progress: number, state: string) => void, gameInfo: { platformSlug?: string; }) + { + return plugins.hooks.games.prePlay.promise({ + source: this.gameSource ?? this.gameId.source, + id: this.gameSourceId ?? this.gameId.id, + saveFolderSlots: this.saveSlots, + command: this.validCommand, + setProgress: setProgress, + gameInfo + }); + } + + async start (context: JobContext, string>, z.infer, string>) + { + let gameInfo: { name?: string, source_id?: string, source?: string; platformSlug?: string; } | undefined = undefined; + if (this.gameId.source === 'emulator') + { + gameInfo = { name: this.gameId.id }; + } else + { + const localGame = await db.query.games.findFirst({ + where: eq(appSchema.games.id, Number(this.gameId.id)), columns: { + name: true, + source_id: true, + source: true, + }, + with: { + platform: { + columns: { + es_slug: true, + slug: true + } + } + } + }); + + if (localGame) + gameInfo = { + name: localGame.name ?? undefined, + source_id: localGame.source_id ?? undefined, + source: localGame.source ?? undefined, + platformSlug: localGame.platform.es_slug ?? localGame.platform.slug + }; + } + + const commandArgs = await plugins.hooks.games.emulatorLaunch.promise({ + autoValidCommand: this.validCommand, + game: { + source: this.gameSource, + sourceId: this.gameSourceId, + id: this.gameId, + platformSlug: gameInfo?.platformSlug + }, + dryRun: false + }); + + await new Promise(async (resolve, reject) => + { + try + { + let game: any; + if (!commandArgs) + { + await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug }).catch(e => reject(e)); + + if (Array.isArray(this.validCommand.command)) + { + let command = this.validCommand.command; + if (process.env.FLATPAK_BUILD) command = ['flatpak-spawn', '--host', `--directory=${config.get('downloadPath')}`, ...command]; + + const bunGame = Bun.spawn(command, { + cwd: this.validCommand.startDir, + signal: context.abortSignal, + env: { + ...process.env, + ...this.validCommand.env + }, + onExit (subprocess, exitCode, signalCode, error) + { + if (error) + { + console.error(error); + reject(error); + } else + { + resolve(true); + } + }, + }); + + context.setProgress(0, "playing"); + + game = bunGame; + } else + { + + let command = this.validCommand.command; + + if (process.env.FLATPAK_BUILD) command = `flatpak-spawn --host --directory=${config.get('downloadPath')} ${command}`; + + // ES-DE commands require shell execution. Some emulators fail otherwise. + const spawnGame = spawn(command, { + shell: this.validCommand.shell ?? true, + cwd: this.validCommand.startDir, + signal: context.abortSignal, + env: { + ...process.env, + ...this.validCommand.env + }, + }); + + context.setProgress(0, "playing"); + + spawnGame.stdout.on('data', data => console.log(data)); + spawnGame.on('close', (code) => + { + resolve(code); + }); + spawnGame.on('error', e => + { + console.error(e); + resolve(1); + }); + + game = spawnGame; + } + } + else if (this.validCommand.metadata.emulatorBin) + { + this.saveSlots = commandArgs.savesPath ?? {}; + + await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug }); + + let command = [this.validCommand.metadata.emulatorBin, ...commandArgs.args]; + if (process.env.FLATPAK_BUILD) command = ['flatpak-spawn', '--host', `--directory=${config.get('downloadPath')}`, ...command]; + + // We have full control over launching integrated emulators better to use bun spawn + const bunGame = Bun.spawn(command, { + cwd: this.validCommand.startDir, + signal: context.abortSignal, + env: { + ...process.env, + ...commandArgs.env + }, + onExit (subprocess, exitCode, signalCode, error) + { + if (error) + { + console.error(error); + reject(error); + } else + { + resolve(true); + } + }, + }); + + context.setProgress(0, "playing"); + + // TODO: this isn't really useful, maybe add it later if needed + /*if (commandArgs.savesPath && await fs.exists(commandArgs.savesPath)) + { + const savesWatcher = watch(commandArgs.savesPath, { recursive: true, signal: context.abortSignal }); + console.log("Starting To Watch", commandArgs.savesPath, "for save file changes"); + savesWatcher.on('change', (type, filename) => + { + if (typeof filename === 'string') + { + console.log("Save File Changed", filename); + this.changedSaveFiles.set(filename, { subPath: filename, cwd: commandArgs.savesPath! }); + } + }); + + bunGame.exited.then(() => + { + savesWatcher.close(); + console.log("Closing Save File Watching for", commandArgs.savesPath); + }); + }*/ + + game = bunGame; + + } else + { + reject(new Error("No Emulator Bin")); + return; + } + + this.activeGame = { + process: game, + name: gameInfo?.name ?? "Unknown", + gameId: this.gameId, + source: this.gameSource, + sourceId: this.gameSourceId, + command: this.validCommand + }; + } catch (e) + { + context.abort(e); + resolve(e); + } + }); + + await this.postPlay({ platformSlug: gameInfo?.platformSlug }); + } + + exposeData () + { + return this.activeGame; + } + +} \ No newline at end of file diff --git a/src/bun/api/jobs/login-job.ts b/src/bun/api/jobs/login-job.ts index eec8a91..fb5d69a 100644 --- a/src/bun/api/jobs/login-job.ts +++ b/src/bun/api/jobs/login-job.ts @@ -1,5 +1,5 @@ import Elysia, { status } from "elysia"; -import { IJob, JobContext } from "../task-queue"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; import { LOGIN_PORT, SERVER_URL } from "@/shared/constants"; import { host, localIp } from "@/bun/utils/host"; import cors from "@elysiajs/cors"; @@ -8,7 +8,7 @@ import { config } from "../app"; import z from "zod"; import { delay } from "@/shared/utils"; -export class LoginJob implements IJob +export class LoginJob implements IJob, "base"> { endsAt: Date; startedAt: Date; @@ -25,7 +25,7 @@ export class LoginJob implements IJob exposeData = (): z.infer => ({ endsAt: this.endsAt, startedAt: this.startedAt, url: this.url }); - async start (context: JobContext): Promise + async start (context: JobContext, "base">): Promise { const loginServer = new Elysia({ serve: { hostname: localIp, port: LOGIN_PORT } }) .use(cors()) @@ -37,7 +37,7 @@ export class LoginJob implements IJob .post(`/login`, async ({ body }) => { const response = await tryLoginAndSave(body as any); - if (response?.code === 200) + if (response.response.ok) { context.abort("success"); return status("Accepted"); diff --git a/src/bun/api/jobs/plugin-operation-job.ts b/src/bun/api/jobs/plugin-operation-job.ts new file mode 100644 index 0000000..db39819 --- /dev/null +++ b/src/bun/api/jobs/plugin-operation-job.ts @@ -0,0 +1,62 @@ +import z from "zod"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; +import { plugins } from "../app"; +import { canUninstall, runBunPackageCommand } from "../plugins/services"; +import { getPlugin, registerPlugin, unregisterPlugin } from "../plugins/register-plugins"; +import { PluginRegistry } from "@/shared/constants"; + +export default class PluginOperationJob implements IJob +{ + static id = "plugin-operation-job" as const; + static dataSchema = z.never(); + group = "plugin-operations"; + operation: "add" | "update" | "remove"; + plugin: string; + + constructor(operation: "add" | "update" | "remove", plugin: string) + { + this.plugin = plugin; + this.operation = operation; + } + + async start (context: JobContext, never, string>) + { + switch (this.operation) + { + case "add": + //TODO: find the latest compatible version with the current sdk version + const addResponse = await runBunPackageCommand(["add", this.plugin, '--omit', 'peer', "--registry", PluginRegistry]); + console.log(addResponse); + const addPlugin = await getPlugin(this.plugin, plugins); + if (!addPlugin) throw new Error(`${this.plugin} Not Found`); + await registerPlugin(addPlugin, 'store', plugins); + break; + case "update": + const existingPlugin = plugins.plugins[this.plugin]; + if (!existingPlugin) throw new Error(`${this.plugin} Not Found`); + if (!existingPlugin.update?.new) throw new Error(`No Update Found`); + let updatePlugin = await getPlugin(this.plugin, plugins); + if (!updatePlugin) throw new Error(`${this.plugin} Not Found`); + await unregisterPlugin(this.plugin, plugins); + const updateResponse = await runBunPackageCommand(["update", `${this.plugin}@${existingPlugin.update?.new}`, '--omit', 'peer', "--registry", PluginRegistry, '--latest']); + console.log(updateResponse); + updatePlugin = await getPlugin(this.plugin, plugins); + if (!updatePlugin) throw new Error(`Something Went Wrong during update. Missing Plugin: ${this.plugin}`); + await registerPlugin(updatePlugin, existingPlugin.source, plugins); + break; + case "remove": + const removePlugin = plugins.plugins[this.plugin]; + if (!removePlugin) throw new Error(`${this.plugin} Not Found`); + if (!canUninstall(removePlugin.description, removePlugin.source)) + { + throw new Error("Uninstall Not Allowed"); + } + const response = await runBunPackageCommand(['remove', this.plugin, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + await unregisterPlugin(this.plugin, plugins); + break; + } + + + } +} \ No newline at end of file diff --git a/src/bun/api/jobs/reload-plugins-job.ts b/src/bun/api/jobs/reload-plugins-job.ts new file mode 100644 index 0000000..5e404d3 --- /dev/null +++ b/src/bun/api/jobs/reload-plugins-job.ts @@ -0,0 +1,15 @@ +import z from "zod"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; +import { plugins } from "../app"; + +export default class ReloadPluginsJob implements IJob +{ + static id = "reload-plugins-job" as const; + static dataSchema = z.never(); + group = "reload-plugins"; + + async start (context: JobContext, never, string>) + { + await plugins.reloadAll(context); + } +} \ No newline at end of file diff --git a/src/bun/api/jobs/self-update-job.ts b/src/bun/api/jobs/self-update-job.ts new file mode 100644 index 0000000..ca2684e --- /dev/null +++ b/src/bun/api/jobs/self-update-job.ts @@ -0,0 +1,121 @@ +import z from "zod"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; +import { events } from "../app"; +import { Downloader } from "@/bun/utils/downloader"; +import path from 'node:path'; +import os from "node:os"; +import winUpdateScript from '@/bun/utils/update-gameflow-windows.bat' with { type: "text" }; +import linuxUpdateScript from '@/bun/utils/update-gameflow-linux.sh' with { type: "text" }; +import mustache from "mustache"; +import pkg from '~/package.json'; +import { sleep } from "bun"; + +export default class SelfUpdateJob implements IJob +{ + static id = "self-update-job" as const; + static dataSchema = z.never(); + group = "self-update"; + + async downloadUpdate (url: URL, dest: string | undefined, filename: string, ctx: JobContext, never, string>) + { + const downloader = new Downloader('update', + [{ + url: url, + file_path: "", + file_name: filename + }], + dest, + { + onProgress (stats) + { + ctx.setProgress(stats.progress, "Downloading Update"); + }, + }); + return downloader.start(); + } + + async start (context: JobContext, never, string>) + { + context.setProgress(0, "Downloading Update"); + await sleep(1000); + const latest = await fetch('https://api.github.com/repos/simeonradivoev/gameflow-deck/releases/latest'); + if (latest.ok) + { + const data = await latest.json(); + let validAsset: any | undefined; + switch (process.platform) + { + case "win32": + validAsset = data.assets.find((e: any) => new Bun.Glob(`Gameflow-${process.platform}-${process.arch}.zip`).match(e.name)); + if (!validAsset) + { + validAsset = data.assets.find((e: any) => new Bun.Glob(`Gameflow-*.zip`).match(e.name)); + } + break; + case "linux": + validAsset = data.assets.find((e: any) => new Bun.Glob(`Gameflow-${process.platform}-${process.arch}.AppImage`).match(e.name)); + if (!validAsset) + { + validAsset = data.assets.find((e: any) => new Bun.Glob(`*.AppImage`).match(e.name)); + } + break; + default: + events.emit('notification', { message: "Unsupported Platfrom", title: 'Failed Update', type: "error" }); + return; + } + + if (!validAsset) + { + events.emit('notification', { message: "Could not find download", title: 'Failed Update', type: "error" }); + return; + } + + console.log("Found Download", validAsset.browser_download_url); + console.log("Starting Download"); + + switch (process.platform) + { + case "linux": + const appimage = process.env.APPIMAGE; + if (!appimage) + { + events.emit('notification', { + message: "Only AppImage supported", + title: 'Failed Update', + type: 'error' + }); + return; + } + const linuxDownloads = await this.downloadUpdate(new URL(validAsset.browser_download_url), undefined, path.basename(appimage), context); + if (!linuxDownloads) return; + const shPath = path.join(os.tmpdir(), "update-gameflow.sh"); + await Bun.write(shPath, mustache.render(linuxUpdateScript, { + tempFile: linuxDownloads[0], + appImagePath: appimage + })); + context.setProgress(0, "Restarting App To Update"); + events.emit('exitapp'); + Bun.spawn(["bash", shPath], { detached: true }); + process.exit(0); + case "win32": + const winDownloads = await this.downloadUpdate(new URL(validAsset.browser_download_url), undefined, "Gameflow-update.zip", context); + if (!winDownloads) return; + const batPath = path.join(os.tmpdir(), "update-gameflow.bat"); + await Bun.write(batPath, mustache.render(winUpdateScript, { + tempFile: winDownloads[0], + installDir: path.dirname(process.execPath), + extractDir: path.join(os.tmpdir(), 'gameflow-update-extract'), + exePath: `${pkg.bin}.exe` + })); + context.setProgress(0, "Restarting App To Update"); + events.emit('exitapp'); + Bun.spawn(["cmd", "/c", "start", "cmd", "/c", batPath], { detached: true }); + process.exit(0); + } + + } else + { + events.emit('notification', { message: latest.statusText, title: 'Failed Update', type: "error" }); + } + } +} \ No newline at end of file diff --git a/src/bun/api/jobs/test-download-job.ts b/src/bun/api/jobs/test-download-job.ts new file mode 100644 index 0000000..313b00b --- /dev/null +++ b/src/bun/api/jobs/test-download-job.ts @@ -0,0 +1,30 @@ +import { DownloadJobData } from "@simeonradivoev/gameflow-sdk/shared"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; +import { sleep } from "bun"; + +export class TestDownloadJob implements IJob +{ + data: DownloadJobData = { + speed: 1686, + downloaded: 0, + total: 6615841, + name: "Test Download Job" + }; + + group = "test-download"; + + async start (context: JobContext, DownloadJobData, string>): Promise + { + for (let i = 0; i < 10; i++) + { + await sleep(1000); + context.setProgress(i / 10 * 100, 'download'); + if (context.abortSignal.aborted) return; + } + } + exposeData (): DownloadJobData + { + return this.data; + } + +} \ No newline at end of file diff --git a/src/bun/api/jobs/twitch-login-job.ts b/src/bun/api/jobs/twitch-login-job.ts index 3d2a0c0..42d98a9 100644 --- a/src/bun/api/jobs/twitch-login-job.ts +++ b/src/bun/api/jobs/twitch-login-job.ts @@ -1,8 +1,9 @@ -import { IJob, JobContext } from "../task-queue"; +import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; import secrets from "../secrets"; import open from "open"; import z from "zod"; import { delay } from "@/shared/utils"; +import { plugins } from "../app"; interface TwitchDevice @@ -16,7 +17,9 @@ interface TwitchDevice verification_uri: string; } -export default class TwitchLoginJob implements IJob +type States = "Retrieving Device" | "Waiting For Authentication"; + +export default class TwitchLoginJob implements IJob, States> { twitchScopes = "analytics:read:extensions analytics:read:games user:read:email"; device?: TwitchDevice; @@ -38,7 +41,7 @@ export default class TwitchLoginJob implements IJob user_code: this.device.user_code }) : undefined; - async start (context: JobContext): Promise + async start (context: JobContext, States>): Promise { context.setProgress(0, "Retrieving Device"); let res = await fetch("https://id.twitch.tv/oauth2/device", { @@ -92,6 +95,8 @@ export default class TwitchLoginJob implements IJob secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token }); secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token }); secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() }); + + await plugins.hooks.auth.loginComplete.promise({ service: 'twitch' }); break; } else if (res.status !== 400) diff --git a/src/bun/api/jobs/update-store.ts b/src/bun/api/jobs/update-store.ts deleted file mode 100644 index ce97c27..0000000 --- a/src/bun/api/jobs/update-store.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ensureDir } from "fs-extra"; -import { IJob, JobContext } from "../task-queue"; -import { getStoreFolder } from "../store/store"; - -export default class UpdateStoreJob implements IJob -{ - static id = "update-store" as const; - static origin = "https://github.com/simeonradivoev/gameflow-store.git"; - static branch = "master"; - - async gitCommand (commands: string[], dir: string) - { - const proc = Bun.spawn(['git', ...commands], { - cwd: dir, - stdout: "pipe", - stderr: "pipe", - }); - - const [output] = await Promise.all([ - new Response(proc.stdout).text(), - proc.exited, - ]); - - return output.trim(); - } - - async isGitRepo (dir: string) - { - return (await this.gitCommand(["rev-parse", "--is-inside-work-tree"], dir)) === 'true'; - } - - async getOrigin (dir: string) - { - const origin = await this.gitCommand(["remote", "get-url", "origin"], dir); - return origin; - } - - async hasChanges (dir: string) - { - return (await this.gitCommand(["status", "--porcelain"], dir)).length > 0; - } - - async start (context: JobContext) - { - const storeFolder = getStoreFolder(); - await ensureDir(storeFolder); - context.setProgress(10); - if (await this.isGitRepo(storeFolder)) - { - const existingOrigin = await this.getOrigin(storeFolder); - if (existingOrigin !== UpdateStoreJob.origin) - { - throw new Error(`Git Repo in downloads is not valid. It has origin of ${existingOrigin}. Repo must be of ${UpdateStoreJob.origin}`); - } - - // check for uncommitted changes - const status = await this.gitCommand([" status", "--porcelain"], storeFolder); - if (status.length > 0) - { - console.log("Cleaning local changes..."); - await this.gitCommand(["reset", "--hard"], storeFolder); - await this.gitCommand(["clean", "-fd"], storeFolder); - } - - // fetch & reset to remote - await this.gitCommand(["fetch", "origin"], storeFolder); - await this.gitCommand(["reset", "--hard", `origin/${UpdateStoreJob.branch}`], storeFolder); - console.log("Shop Repo updated"); - } else - { - context.setProgress(50); - await this.gitCommand(["clone", "--depth", "1", "--branch", UpdateStoreJob.branch, UpdateStoreJob.origin, '.'], storeFolder); - context.setProgress(100); - } - } -} \ No newline at end of file diff --git a/src/bun/api/notifications.ts b/src/bun/api/notifications.ts index c20a67d..e1c135c 100644 --- a/src/bun/api/notifications.ts +++ b/src/bun/api/notifications.ts @@ -1,4 +1,5 @@ -import { Notification } from '@shared/constants'; + +import { FrontendNotification } from '@simeonradivoev/gameflow-sdk/shared'; import { events } from './app'; export default function buildNotificationsStream () @@ -10,7 +11,7 @@ export default function buildNotificationsStream () { const encoder = new TextEncoder(); - function enqueue (data: Notification, event?: 'notification') + function enqueue (data: FrontendNotification, event?: 'notification') { const evntString = event ? `event: ${event}\n` : ''; controller.enqueue(encoder.encode(`${evntString}data: ${JSON.stringify(data)}\n\n`)); @@ -30,7 +31,7 @@ export default function buildNotificationsStream () } }, 15000); - const notificationHandler = (notification: Notification) => + const notificationHandler = (notification: FrontendNotification) => { enqueue(notification, 'notification'); }; diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu.ts new file mode 100644 index 0000000..a9e6865 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu.ts @@ -0,0 +1,46 @@ +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import path from 'node:path'; +import { config } from "@/bun/api/app"; + +export default class CEMUIntegration implements PluginType +{ + emulator = 'CEMU'; + + async load (ctx: PluginLoadingContextType) + { + ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => + { + return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen"] }; + }); + + ctx.hooks.games.postPlay.tapPromise({ name: desc.name }, async ({ saveFolderSlots, validChangedSaveFiles, command }) => + { + if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; + validChangedSaveFiles[this.emulator] = { + cwd: saveFolderSlots[this.emulator].cwd, + shared: true, + subPath: '*.{tga,xml,dat}', + isGlob: true + }; + }); + + ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => + { + const args: string[] = []; + + args.push(`--fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`); + + const savesPath = path.join(config.get('downloadPath'), "saves", this.emulator); + + args.push(`--mlc=${savesPath}`); + + if (ctx.autoValidCommand.metadata.romPath) + { + args.push(`--game=${ctx.autoValidCommand.metadata.romPath}`); + } + + return { args, savesPath: { [this.emulator]: { cwd: savesPath } } }; + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json new file mode 100644 index 0000000..9a0c5c6 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json @@ -0,0 +1,15 @@ +{ + "name": "com.simeonradivoev.gameflow.cemu", + "displayName": "CEMU Integration", + "version": "0.0.1", + "description": "CEMU Emulator Integration", + "main": "./cemu.ts", + "icon": "https://upload.wikimedia.org/wikipedia/commons/9/9e/Cemu_Emulator_Official_Logo.png", + "category": "emulators", + "keywords": [ + "integration", + "emulator", + "wiiu", + "cemu" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts new file mode 100644 index 0000000..6a44901 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts @@ -0,0 +1,87 @@ + +import { config } from "@/bun/api/app"; +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import path from 'node:path'; +import desc from './package.json'; +import { ensureDir } from "fs-extra"; +import { getSavePaths, getType } from "./utils"; + +export default class DOLPHINIntegration implements PluginType +{ + emulator = 'DOLPHIN'; + + async load (ctx: PluginLoadingContextType) + { + ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => + { + return { id: desc.name, supportLevel: "full", capabilities: ["batch", "config", "resolution", "fullscreen", "saves"] }; + }); + + ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => + { + await Bun.write(path.join(ctx.path, "portable.txt"), ""); + }); + + ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => + { + const args: string[] = []; + + const storageFolder = path.join(config.get('downloadPath'), "storage", this.emulator); + args.push(`--user=${storageFolder}`); + + args.push(`--config=Dolphin.Display.Fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`); + args.push(`--config=Dolphin.General.ISOPath0=${path.join(config.get('downloadPath'), 'roms', 'gc')}`); + args.push(`--config=Dolphin.General.ISOPath1=${path.join(config.get('downloadPath'), 'roms', 'wii')}`); + args.push(`--config=Dolphin.Interface.ConfirmStop=False`); + args.push(`--config=Dolphin.Interface.SkipNKitWarning=True`); + args.push(`--config=Dolphin.Analytics.PermissionAsked=True`); + + const resolution = config.get('emulatorResolution'); + const resolutionMapping = { + "720p": 2, + "1080p": 3, + "1440p": 4, + "4k": 6 + }; + args.push(`--config=GFX.Settings.InternalResolution=${resolutionMapping[resolution] ?? 1}`); + args.push(`--config=GFX.Settings.wideScreenHack=${config.get('emulatorWidescreen') ? "True" : "False"}`); + args.push(`--config=GFX.Settings.AspectRatio=${config.get('emulatorWidescreen') ? "1" : "0"}`); + + const savesPath = path.join(config.get('downloadPath'), "saves", this.emulator); + + args.push(`--config=Dolphin.General.WiiSDCardPath=${path.join(savesPath, 'WiiSD.raw')}`); + args.push(`--config=Dolphin.General.WiiSDCardSyncFolder=${path.join(savesPath, 'WiiSDSync')}`); + args.push(`--config=Dolphin.GBA.SavesPath=${path.join(savesPath, 'GBA')}`); + args.push(`--config=Dolphin.Core.GCIFolderAPath=${path.join(savesPath, 'GC')}`); + + if (!ctx.dryRun) + { + await ensureDir(path.join(savesPath, 'GC', "JAP")); + await ensureDir(path.join(savesPath, 'GC', "EUR")); + await ensureDir(path.join(savesPath, 'GC', "USA")); + } + + let finalSavesPath: string | undefined = undefined; + if (ctx.autoValidCommand.metadata.romPath) + { + args.push("--batch"); + args.push(`--exec=${ctx.autoValidCommand.metadata.romPath}`); + + finalSavesPath = await getType(ctx.autoValidCommand.metadata.romPath, ctx.autoValidCommand.metadata.emulatorDir) === 'gamecube' ? savesPath : storageFolder; + return { args, savesPath: { [this.emulator]: { cwd: finalSavesPath } } }; + } + + return { args }; + }); + + ctx.hooks.games.postPlay.tap({ name: desc.name }, async ({ validChangedSaveFiles, saveFolderSlots, command }) => + { + if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; + validChangedSaveFiles[this.emulator] = { + cwd: saveFolderSlots[this.emulator].cwd, + subPath: await getSavePaths(command.metadata.romPath, saveFolderSlots.dolphin.cwd, command.metadata.emulatorDir), + shared: false + }; + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json new file mode 100644 index 0000000..e413d06 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json @@ -0,0 +1,16 @@ +{ + "name": "com.simeonradivoev.gameflow.dolphin", + "displayName": "DOLPHIN Integration", + "version": "0.0.1", + "description": "DOLPHIN Emulator Integration", + "main": "./dolphin.ts", + "icon": "https://upload.wikimedia.org/wikipedia/commons/5/53/Dolphin_Emulator_Logo_Refresh.svg", + "category": "emulators", + "keywords": [ + "integration", + "emulator", + "wii", + "gc", + "dolphin" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/utils.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/utils.ts new file mode 100644 index 0000000..8794e80 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/utils.ts @@ -0,0 +1,164 @@ +import { join } from "path"; +import { platform } from "os"; +import fs from "node:fs/promises"; +import path from "node:path"; + +type DolphinLocation = + | { type: "path"; toolPath: string; } + | { type: "appimage"; appImagePath: string; }; + +async function findDolphinTool (bundledDir?: string): Promise +{ + const os = platform(); + const toolName = os === "win32" ? "DolphinTool.exe" : "dolphin-tool"; + + if (bundledDir) + { + if (os === "linux") + { + const glob = new Bun.Glob("*.AppImage"); + for await (const file of glob.scan(bundledDir)) + { + return { type: "appimage", appImagePath: join(bundledDir, file) }; + } + throw new Error(`No AppImage found in ${bundledDir}`); + } else + { + return { type: "path", toolPath: join(bundledDir, toolName) }; + } + } + + // Fallback 1: check PATH + const inPath = Bun.which(toolName); + if (inPath) return { type: "path", toolPath: inPath }; + + // Fallback 2: platform default install locations + if (os === "win32") + { + const candidates = [ + "C:/Program Files/Dolphin/DolphinTool.exe", + "C:/Program Files (x86)/Dolphin/DolphinTool.exe", + ]; + for (const candidate of candidates) + { + if (await Bun.file(candidate).exists()) + { + return { type: "path", toolPath: candidate }; + } + } + } else if (os === "darwin") + { + const candidate = "/Applications/Dolphin.app/Contents/MacOS/dolphin-tool"; + if (await Bun.file(candidate).exists()) + { + return { type: "path", toolPath: candidate }; + } + } else if (os === "linux") + { + const home = process.env.HOME ?? ""; + const candidates = [ + join(home, "Applications/Dolphin-x86_64.AppImage"), + join(home, "Applications/Dolphin.AppImage"), + "/opt/Dolphin-x86_64.AppImage", + ]; + for (const candidate of candidates) + { + if (await Bun.file(candidate).exists()) + { + return { type: "appimage", appImagePath: candidate }; + } + } + } + + throw new Error(`Could not find ${toolName}. Install Dolphin or pass its folder path explicitly.`); +} + +async function runDolphinTool (args: string[], location: DolphinLocation): Promise +{ + if (location.type === "path") + { + const proc = Bun.spawnSync([location.toolPath, ...args]); + if (!proc.success) throw new Error(`dolphin-tool failed: ${proc.stderr.toString()}`); + return proc.stdout.toString(); + } else + { + const mount = Bun.spawn([location.appImagePath, "--appimage-mount"], { + stdout: "pipe", + stderr: "pipe", + }); + const mountPoint = (await new Response(mount.stdout).text()).trim(); + try + { + const proc = Bun.spawnSync([`${mountPoint}/usr/bin/dolphin-tool`, ...args]); + if (!proc.success) throw new Error(`dolphin-tool failed: ${proc.stderr.toString()}`); + return proc.stdout.toString(); + } finally + { + mount.kill(); + } + } +} + +async function readGameId (romPath: string, location: DolphinLocation): Promise +{ + const output = await runDolphinTool(["header", "-i", romPath], location); + const match = output.match(/Game ID:\s*(\w{6})/); + if (!match) throw new Error("Could not read game ID"); + return match[1]; +} + +function getRegion (regionCode: string) +{ + switch (regionCode) + { + case "E": return "USA"; + case "P": return "EUR"; + case "J": return "JAP"; + default: return "USA"; + } +} + +async function getGCSavePaths (romPath: string, savesPath: string, location: DolphinLocation) +{ + const gameId = await readGameId(romPath, location); + const region = getRegion(gameId[3]); + + const makerCode = gameId.slice(4, 6); // e.g. "01" or "7D" — already the right format + const gameCode = gameId.slice(0, 4); // e.g. "GZLE" or "GM5E" + const cardPath = join(savesPath, "GC", region); + + const glob = new Bun.Glob(`${makerCode}-${gameCode}-*.gci`); + const saves: string[] = []; + for await (const file of glob.scan(cardPath)) + { + saves.push(path.join("GC", region, file)); + } + + return saves; +} + +export async function getType (romPath: string, bundledEmulatorDir?: string): Promise<"gamecube" | "wii"> +{ + const location = await findDolphinTool(bundledEmulatorDir); + const gameId = await readGameId(romPath, location); + const isGameCube = gameId[0] === "G" || gameId[0] === "D"; + return isGameCube ? "gamecube" : "wii"; +} + +export async function getSavePaths (romPath: string, savesPath: string, bundledEmulatorDir?: string): Promise +{ + const location = await findDolphinTool(bundledEmulatorDir); + const gameId = await readGameId(romPath, location); + const isGameCube = gameId[0] === "G" || gameId[0] === "D"; + + if (isGameCube) + { + return getGCSavePaths(romPath, savesPath, location); + } else + { + const folder = Buffer.from(gameId.slice(0, 4), "ascii").toString("hex").toUpperCase(); + const rootFolder = join(savesPath, "Wii", "title", "00010000", folder); + const files = await fs.readdir(rootFolder, { recursive: true }); + return files.map(f => path.join("Wii", "title", "00010000", f)); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/PCSX2.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/PCSX2.ini new file mode 100644 index 0000000..e1403c5 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/PCSX2.ini @@ -0,0 +1,476 @@ +[UI] +SettingsVersion = 1 +InhibitScreensaver = true +ConfirmShutdown = true +StartPaused = false +PauseOnFocusLoss = false +StartFullscreen = false +DoubleClickTogglesFullscreen = true +HideMouseCursor = false +RenderToSeparateWindow = false +HideMainWindowWhenRunning = false +DisableWindowResize = false +Theme = darkfusion +SetupWizardIncomplete = false + + +[EmuCore] +CdvdVerboseReads = false +CdvdDumpBlocks = false +CdvdShareWrite = false +EnablePatches = true +EnableCheats = false +EnablePINE = false +EnableNoInterlacingPatches = false +EnableRecordingTools = true +EnableGameFixes = true +SaveStateOnShutdown = false +EnableDiscordPresence = false +InhibitScreensaver = true +ConsoleToStdio = false +HostFs = false +BackupSavestate = true +SavestateZstdCompression = true +McdEnableEjection = true +McdFolderAutoManage = true +WarnAboutUnsafeSettings = true +GzipIsoIndexTemplate = $(f).pindex.tmp +BlockDumpSaveDirectory = +EnableFastBoot = true + + +[EmuCore/Speedhacks] +EECycleRate = 0 +EECycleSkip = 0 +fastCDVD = false +IntcStat = true +WaitLoop = true +vuFlagHack = true +vuThread = true +vu1Instant = true + + +[EmuCore/CPU] +FPU.DenormalsAreZero = true +FPU.FlushToZero = true +FPU.Roundmode = 3 +AffinityControlMode = 0 +VU0.DenormalsAreZero = true +VU0.FlushToZero = true +VU0.Roundmode = 3 +VU1.DenormalsAreZero = true +VU1.FlushToZero = true +VU1.Roundmode = 3 + + +[EmuCore/CPU/Recompiler] +EnableEE = true +EnableIOP = true +EnableEECache = false +EnableVU0 = true +EnableVU1 = true +EnableFastmem = true +PauseOnTLBMiss = false +vu0Overflow = true +vu0ExtraOverflow = false +vu0SignOverflow = false +vu0Underflow = false +vu1Overflow = true +vu1ExtraOverflow = false +vu1SignOverflow = false +vu1Underflow = false +fpuOverflow = true +fpuExtraOverflow = false +fpuFullMode = false + + +[EmuCore/GS] +VsyncQueueSize = 2 +FrameLimitEnable = true +VsyncEnable = 0 +FramerateNTSC = 59.94 +FrameratePAL = 50 +SyncToHostRefreshRate = false +AspectRatio = {{ASPECT_RATIO}} +FMVAspectRatioSwitch = Off +ScreenshotSize = 0 +ScreenshotFormat = 0 +ScreenshotQuality = 50 +StretchY = 100 +CropLeft = 0 +CropTop = 0 +CropRight = 0 +CropBottom = 0 +pcrtc_antiblur = true +disable_interlace_offset = false +pcrtc_offsets = false +pcrtc_overscan = false +IntegerScaling = false +UseDebugDevice = false +UseBlitSwapChain = false +disable_shader_cache = false +DisableDualSourceBlend = false +DisableFramebufferFetch = false +DisableThreadedPresentation = false +SkipDuplicateFrames = false +OsdShowMessages = true +OsdShowSpeed = false +OsdShowFPS = false +OsdShowCPU = false +OsdShowGPU = false +OsdShowResolution = false +OsdShowGSStats = false +OsdShowIndicators = true +OsdShowSettings = false +OsdShowInputs = false +OsdShowFrameTimes = false +HWSpinGPUForReadbacks = false +HWSpinCPUForReadbacks = false +paltex = false +autoflush_sw = true +preload_frame_with_gs_data = false +mipmap = true +UserHacks = false +UserHacks_align_sprite_X = false +UserHacks_AutoFlush = false +UserHacks_CPU_FB_Conversion = false +UserHacks_ReadTCOnClose = false +UserHacks_DisableDepthSupport = false +UserHacks_DisablePartialInvalidation = false +UserHacks_Disable_Safe_Features = false +UserHacks_merge_pp_sprite = false +UserHacks_WildHack = false +UserHacks_TextureInsideRt = 0 +UserHacks_TargetPartialInvalidation = false +UserHacks_EstimateTextureRegion = false +fxaa = false +ShadeBoost = false +dump = false +save = false +savef = false +savet = false +savez = false +DumpReplaceableTextures = false +DumpReplaceableMipmaps = false +DumpTexturesWithFMVActive = false +DumpDirectTextures = true +DumpPaletteTextures = true +LoadTextureReplacements = false +LoadTextureReplacementsAsync = true +PrecacheTextureReplacements = false +EnableVideoCapture = true +EnableVideoCaptureParameters = false +VideoCaptureAutoResolution = false +EnableAudioCapture = true +EnableAudioCaptureParameters = false +linear_present_mode = 1 +deinterlace_mode = 0 +OsdScale = 100 +Renderer = 14 +mipmap_hw = -1 +accurate_blending_unit = 1 +crc_hack_level = -1 +filter = 2 +texture_preloading = 2 +GSDumpCompression = 2 +HWDownloadMode = 0 +CASMode = 0 +CASSharpness = 50 +dithering_ps2 = 2 +MaxAnisotropy = 0 +extrathreads = 3 +extrathreads_height = 4 +TVShader = 0 +UserHacks_SkipDraw_Start = 0 +UserHacks_SkipDraw_End = 0 +UserHacks_Half_Bottom_Override = -1 +UserHacks_HalfPixelOffset = 0 +UserHacks_round_sprite_offset = 0 +UserHacks_TCOffsetX = 0 +UserHacks_TCOffsetY = 0 +UserHacks_CPUSpriteRenderBW = 0 +UserHacks_CPUCLUTRender = 0 +UserHacks_GPUTargetCLUTMode = 0 +TriFilter = -1 +OverrideTextureBarriers = -1 +OverrideGeometryShaders = -1 +ShadeBoost_Brightness = 50 +ShadeBoost_Contrast = 50 +ShadeBoost_Saturation = 50 +png_compression_level = 1 +saven = 0 +savel = 5000 +CaptureContainer = mp4 +VideoCaptureCodec = +VideoCaptureParameters = +AudioCaptureCodec = +AudioCaptureParameters = +VideoCaptureBitrate = 6000 +VideoCaptureWidth = 640 +VideoCaptureHeight = 480 +AudioCaptureBitrate = 160 +Adapter = (Default) +HWDumpDirectory = +SWDumpDirectory = + + +[SPU2/Debug] +Global_Enable = false +Show_Messages = false +Show_Messages_Key_On_Off = false +Show_Messages_Voice_Off = false +Show_Messages_DMA_Transfer = false +Show_Messages_AutoDMA = false +Show_Messages_Overruns = false +Show_Messages_CacheStats = false +Log_Register_Access = false +Log_DMA_Transfers = false +Log_WAVE_Output = false +Dump_Info = false +Dump_Memory = false +Dump_Regs = false + + +[SPU2/Mixing] +FinalVolume = 100 + + +[SPU2/Output] +OutputModule = cubeb +BackendName = +DeviceName = +Latency = 60 +OutputLatency = 20 +OutputLatencyMinimal = false +SynchMode = 0 +SpeakerConfiguration = 0 +DplDecodingLevel = 0 + + +[DEV9/Eth] +EthEnable = false +EthApi = Unset +EthDevice = +EthLogDNS = false +InterceptDHCP = false +PS2IP = 0.0.0.0 +Mask = 0.0.0.0 +Gateway = 0.0.0.0 +DNS1 = 0.0.0.0 +DNS2 = 0.0.0.0 +AutoMask = true +AutoGateway = true +ModeDNS1 = Auto +ModeDNS2 = Auto + + +[DEV9/Eth/Hosts] +Count = 0 + + +[DEV9/Hdd] +HddEnable = false +HddFile = DEV9hdd.raw +HddSizeSectors = 83886080 + + +[EmuCore/Gamefixes] +VuAddSubHack = false +FpuMulHack = false +FpuNegDivHack = false +XgKickHack = false +EETimingHack = false +InstantDMAHack = false +SoftwareRendererFMVHack = false +SkipMPEGHack = false +OPHFlagHack = false +DMABusyHack = false +VIFFIFOHack = false +VIF1StallHack = false +GIFFIFOHack = false +GoemonTlbHack = false +IbitHack = false +VUSyncHack = false +VUOverflowHack = false +BlitInternalFPSHack = false +FullVU0SyncHack = false + + +[EmuCore/Profiler] +Enabled = false +RecBlocks_EE = true +RecBlocks_IOP = true +RecBlocks_VU0 = true +RecBlocks_VU1 = true + + +[EmuCore/Debugger] +ShowDebuggerOnStart = false +AlignMemoryWindowStart = true +FontWidth = 8 +FontHeight = 12 +WindowWidth = 0 +WindowHeight = 0 +MemoryViewBytesPerRow = 16 + + +[EmuCore/TraceLog] +Enabled = false +EE.bitset = 0 +IOP.bitset = 0 + + +[USB1] +Type = None + + +[USB2] +Type = None + + +[Achievements] +Enabled = false +TestMode = false +UnofficialTestMode = false +RichPresence = true +ChallengeMode = false +Leaderboards = true +Notifications = true +SoundEffects = true +PrimedIndicators = true + + +[Filenames] +BIOS = + + +[Framerate] +NominalScalar = 1 +TurboScalar = 2 +SlomoScalar = 0.5 + + +[MemoryCards] +Slot1_Enable = true +Slot1_Filename = Mcd001.ps2 +Slot2_Enable = true +Slot2_Filename = Mcd002.ps2 +Multitap1_Slot2_Enable = false +Multitap1_Slot2_Filename = Mcd-Multitap1-Slot02.ps2 +Multitap1_Slot3_Enable = false +Multitap1_Slot3_Filename = Mcd-Multitap1-Slot03.ps2 +Multitap1_Slot4_Enable = false +Multitap1_Slot4_Filename = Mcd-Multitap1-Slot04.ps2 +Multitap2_Slot2_Enable = false +Multitap2_Slot2_Filename = Mcd-Multitap2-Slot02.ps2 +Multitap2_Slot3_Enable = false +Multitap2_Slot3_Filename = Mcd-Multitap2-Slot03.ps2 +Multitap2_Slot4_Enable = false +Multitap2_Slot4_Filename = Mcd-Multitap2-Slot04.ps2 + + +[InputSources] +Keyboard = true +Mouse = true +SDL = true +SDLControllerEnhancedMode = false + + +[Hotkeys] +ToggleFullscreen = SDL-0/Start & SDL-0/LeftStick +CycleInterlaceMode = Keyboard/F5 +CycleMipmapMode = Keyboard/Insert +GSDumpMultiFrame = Keyboard/Control & Keyboard/Shift & Keyboard/F8 +Screenshot = Keyboard/F8 +GSDumpSingleFrame = Keyboard/Shift & Keyboard/F8 +ZoomIn = Keyboard/Control & Keyboard/Plus +ZoomOut = Keyboard/Control & Keyboard/Minus +InputRecToggleMode = Keyboard/Shift & Keyboard/R +LoadStateFromSlot = SDL-0/Back & SDL-0/LeftShoulder +SaveStateToSlot = SDL-0/Back & SDL-0/RightShoulder +ShutdownVM = SDL-0/Back & SDL-0/Start +ToggleFrameLimit = Keyboard/F4 +TogglePause = SDL-0/Back & SDL-0/A +ToggleSlowMotion = SDL-0/Back & SDL-0/+LeftTrigger +ToggleTurbo = SDL-0/Back & SDL-0/+RightTrigger +HoldTurbo = Keyboard/Period +ResetVM = SDL-0/Back & SDL-0/LeftStick +OpenPauseMenu = SDL-0/Back & SDL-0/RightStick +IncreaseUpscaleMultiplier = SDL-0/Start & SDL-0/DPadUp +DecreaseUpscaleMultiplier = SDL-0/Start & SDL-0/DPadDown +CycleAspectRatio = SDL-0/Start & SDL-0/DPadRight +ToggleSoftwareRendering = SDL-0/Start & SDL-0/DPadLeft +ToggleSoftwareRendering = Keyboard/F9 +NextSaveStateSlot = SDL-0/Start & SDL-0/RightShoulder +PreviousSaveStateSlot = SDL-0/Start & SDL-0/LeftShoulder + +[Pad1] +Type = DualShock2 +Deadzone = 0.000000 +AxisScale = 1.330000 +LargeMotorScale = 1.000000 +SmallMotorScale = 1.000000 +PressureModifier = 0.5 +Up = SDL-0/DPadUp +Right = SDL-0/DPadRight +Down = SDL-0/DPadDown +Left = SDL-0/DPadLeft +Triangle = SDL-0/Y +Circle = SDL-0/B +Cross = SDL-0/A +Square = SDL-0/X +Select = SDL-0/Back +Start = SDL-0/Start +L1 = SDL-0/LeftShoulder +L2 = SDL-0/+LeftTrigger +R1 = SDL-0/RightShoulder +R2 = SDL-0/+RightTrigger +L3 = SDL-0/LeftStick +R3 = SDL-0/RightStick +LUp = SDL-0/-LeftY +LRight = SDL-0/+LeftX +LDown = SDL-0/+LeftY +LLeft = SDL-0/-LeftX +RUp = SDL-0/-RightY +RRight = SDL-0/+RightX +RDown = SDL-0/+RightY +RLeft = SDL-0/-RightX +Analog = SDL-0/Guide +LargeMotor = SDL-0/LargeMotor +SmallMotor = SDL-0/SmallMotor +Pressure = Keyboard/S + +[Pad2] +Type = DualShock2 +Deadzone = 0.000000 +AxisScale = 1.330000 +LargeMotorScale = 1.000000 +SmallMotorScale = 1.000000 +PressureModifier = 0.300000 +Up = SDL-1/DPadUp +Right = SDL-1/DPadRight +Down = SDL-1/DPadDown +Left = SDL-1/DPadLeft +Triangle = SDL-1/Y +Circle = SDL-1/B +Cross = SDL-1/A +Square = SDL-1/X +Select = SDL-1/Back +Start = SDL-1/Start +L1 = SDL-1/LeftShoulder +L2 = SDL-1/+LeftTrigger +R1 = SDL-1/RightShoulder +R2 = SDL-1/+RightTrigger +L3 = SDL-1/LeftStick +R3 = SDL-1/RightStick +Analog = SDL-1/Guide +LUp = SDL-1/-LeftY +LRight = SDL-1/+LeftX +LDown = SDL-1/+LeftY +LLeft = SDL-1/-LeftX +RUp = SDL-1/-RightY +RRight = SDL-1/+RightX +RDown = SDL-1/+RightY +RLeft = SDL-1/-RightX +LargeMotor = SDL-1/LargeMotor +SmallMotor = SDL-1/SmallMotor diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json new file mode 100644 index 0000000..6b8c725 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json @@ -0,0 +1,15 @@ +{ + "name": "com.simeonradivoev.gameflow.pcsx2", + "displayName": "PCSX2 Integration", + "version": "0.0.1", + "description": "PCSX2 Emulator Integration", + "main": "./pcsx2.ts", + "icon": "https://upload.wikimedia.org/wikipedia/commons/2/2b/PCSX2_logo4.png", + "category": "emulators", + "keywords": [ + "integration", + "emulator", + "ps2", + "pcsx2" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts new file mode 100644 index 0000000..58d61aa --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts @@ -0,0 +1,125 @@ + +import { config } from "@/bun/api/app"; +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import defaultConfig from './PCSX2.ini' with { type: 'file' }; +import path from 'node:path'; +import { ensureDir } from "fs-extra"; +import desc from './package.json'; +import ini from 'ini'; +import { EmulatorCapabilities } from "@simeonradivoev/gameflow-sdk/shared"; + +export default class PCSX2Integration implements PluginType +{ + emulator = "PCSX2"; + + async load (ctx: PluginLoadingContextType) + { + ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => + { + const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen"]; + + if (ctx.source?.type === 'store') + { + return { + id: desc.name, + supportLevel: "full", + capabilities: [...baseCapabilities, "config", "resolution"] + }; + } + else + { + return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] }; + } + }); + + ctx.hooks.games.postPlay.tapPromise({ name: desc.name }, async ({ saveFolderSlots, validChangedSaveFiles, command }) => + { + if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; + validChangedSaveFiles[this.emulator] = { + cwd: saveFolderSlots[this.emulator].cwd, + shared: true, + subPath: '*.ps2', + isGlob: true, + fixedSize: true + }; + }); + + ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => + { + const args: string[] = []; + if (ctx.autoValidCommand.metadata.romPath) + { + args.push(ctx.autoValidCommand.metadata.romPath); + args.push("-batch"); + } + if (config.get('launchInFullscreen')) + { + args.push("-fullscreen"); + } + args.push(...["-bigpicture", "-portable", "--"]); + + if (ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir && !ctx.dryRun) + { + let pscx2Path = ''; + if (process.platform === 'win32') + pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'inis'); + else + pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, this.emulator, 'inis'); + + const configPath = path.join(pscx2Path, 'PCSX2.ini'); + const existingConfigFile = Bun.file(configPath); + + const configFile = await existingConfigFile.exists() ? ini.parse(await existingConfigFile.text()) : ini.parse(await Bun.file(defaultConfig).text()); + + const biosFolder = path.join(config.get('downloadPath'), "bios", this.emulator); + const storageFolder = path.join(config.get('downloadPath'), "storage", this.emulator); + const savesFolder = path.join(config.get('downloadPath'), "saves", this.emulator); + const resolutionMapping = { + "720p": 2, + "1080p": 3, + "1440p": 4, + "4k": 6, + }; + + const paths = { + BIOS_PATH: biosFolder, + SNAPSHOTS_PATH: path.join(storageFolder, 'snaps'), + SAVE_STATES_PATH: path.join(savesFolder, 'states'), + MEMORY_CARDS_PATH: path.join(savesFolder, 'saves'), + CACHE_PATH: path.join(storageFolder, 'cache'), + COVERS_PATH: path.join(storageFolder, 'covers'), + TEXTURES_PATH: path.join(storageFolder, 'textures'), + VIDEOS_PATH: path.join(storageFolder, 'videos'), + LOGS_PATH: path.join(storageFolder, 'logs'), + RECURSIVE_PATHS: path.join(config.get('downloadPath'), 'roms', 'PS2'), + }; + + await Promise.all(Object.values(paths).map(p => ensureDir(p))); + + configFile.EmuCore ??= {}; + configFile.EmuCore.EnableWideScreenPatches = config.get('emulatorWidescreen'); + configFile['EmuCore/GS'] ??= {}; + configFile['EmuCore/GS'].AspectRatio = config.get('emulatorWidescreen') ? "16:9" : "Auto 4:3/3:2"; + configFile['EmuCore/GS'].upscale_multiplier = resolutionMapping[config.get('emulatorResolution')] ?? 1; + configFile.Folders ??= {}; + configFile.Folders.Bios = paths.BIOS_PATH; + configFile.Folders.Snapshots = paths.SNAPSHOTS_PATH; + configFile.Folders.SaveStates = paths.SAVE_STATES_PATH; + configFile.Folders.MemoryCards = paths.MEMORY_CARDS_PATH; + configFile.Folders.Cache = paths.CACHE_PATH; + configFile.Folders.Covers = paths.COVERS_PATH; + configFile.Folders.Textures = paths.TEXTURES_PATH; + configFile.Folders.Videos = paths.VIDEOS_PATH; + configFile.Folders.Logs = paths.LOGS_PATH; + configFile.GameList ??= {}; + configFile.GameList.RecursivePaths = paths.RECURSIVE_PATHS; + + await Bun.write(configPath, ini.stringify(configFile)); + + return { args, savesPath: { [this.emulator]: { cwd: paths.MEMORY_CARDS_PATH } } }; + } + + return { args }; + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/controls.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/controls.ini new file mode 100644 index 0000000..607f58d --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/controls.ini @@ -0,0 +1,27 @@ +[ControlMapping] +Up = 10-19 +Down = 10-20 +Left = 10-21 +Right = 10-22 +Circle = 10-190 +Cross = 10-189 +Square = 10-191 +Triangle = 10-188 +Start = 10-197 +Select = 10-196 +L = 10-193 +R = 10-192 +An.Up = 10-4003 +An.Down = 10-4002 +An.Left = 10-4001 +An.Right = 10-4000 +Fast-forward = 1-193:10-4010,1-135 +Rewind = 10-196:10-4008 +Save State = 10-196:10-192,1-132 +Load State = 10-196:10-193,1-133 +Previous Slot = 10-197:10-193,1-137 +Next Slot = 10-197:10-192,1-136 +Pause = 10-196:10-107,1-111 +Screenshot = 10-196:10-190 +Exit App = 10-196:10-197 +SpeedToggle = 10-196:10-4010 \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini new file mode 100644 index 0000000..c138918 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini @@ -0,0 +1,477 @@ +[General] +FirstRun = False +RunCount = 4 +Enable Logging = True +AutoRun = True +Browse = False +IgnoreBadMemAccess = True +CurrentDirectory = /home +ShowDebuggerOnLoad = False +CheckForNewVersion = True +Language = en_US +ForceLagSync2 = False +DiscordPresence = True +UISound = False +AutoLoadSaveState = 0 +EnableCheats = True +CwCheatRefreshRate = 77 +CwCheatScrollPosition = 0.000000 +GameListScrollPosition = 0.000000 +ScreenshotsAsPNG = False +UseFFV1 = False +DumpFrames = False +DumpVideoOutput = False +DumpAudio = False +SaveLoadResetsAVdumping = False +StateSlot = 0 +EnableStateUndo = True +StateLoadUndoGame = NA +StateUndoLastSaveGame = NA +StateUndoLastSaveSlot = -5 +RewindFlipFrequency = 0 +ShowOnScreenMessage = True +ShowRegionOnGameIcon = False +ShowIDOnGameIcon = True +GameGridScale = 1.000000 +GridView1 = True +GridView2 = False +GridView3 = False +RightAnalogUp = 0 +RightAnalogDown = 0 +RightAnalogLeft = 0 +RightAnalogRight = 0 +RightAnalogPress = 0 +RightAnalogCustom = False +RightAnalogDisableDiagonal = False +SwipeUp = 0 +SwipeDown = 0 +SwipeLeft = 0 +SwipeRight = 0 +SwipeSensitivity = 1.000000 +SwipeSmoothing = 0.300000 +DoubleTapGesture = 0 +GestureControlEnabled = False +ReportingHost = default +AutoSaveSymbolMap = False +CacheFullIsoInRam = False +RemoteISOPort = 0 +LastRemoteISOServer = +LastRemoteISOPort = 0 +RemoteISOManualConfig = False +RemoteShareOnStartup = False +RemoteISOSubdir = / +RemoteDebuggerOnStartup = False +InternalScreenRotation = 1 +BackgroundAnimation = 1 +PauseWhenMinimized = False +DumpDecryptedEboots = False +MemStickInserted = True +EnablePlugins = True +[CPU] +CPUCore = 1 +SeparateSASThread = True +SeparateIOThread = True +IOTimingMethod = 0 +FastMemoryAccess = True +FunctionReplacements = True +HideSlowWarnings = False +HideStateWarnings = False +PreloadFunctions = False +JitDisableFlags = 0x00000000 +CPUSpeed = 0 +[Graphics] +EnableCardboardVR = False +CardboardScreenSize = 50 +CardboardXShift = 0 +CardboardYShift = 0 +ShowFPSCounter = 0 +GraphicsBackend = 3 (VULKAN) +FailedGraphicsBackends = +DisabledGraphicsBackends = +VulkanDevice = +CameraDevice = +RenderingMode = 1 +SoftwareRenderer = False +HardwareTransform = True +SoftwareSkinning = True +TextureFiltering = 1 +BufferFiltering = 1 +AndroidHwScale = 1 +HighQualityDepth = 1 +FrameSkip = 0 +FrameSkipType = 0 +AutoFrameSkip = False +FrameRate = 0 +FrameRate2 = -1 +UnthrottlingMode = CONTINUOUS +AnisotropyLevel = 4 +VertexDecCache = False +TextureBackoffCache = False +TextureSecondaryCache = False +FullScreenMulti = False +SmallDisplayZoomType = 2 +SmallDisplayOffsetX = 0.500000 +SmallDisplayOffsetY = 0.500000 +SmallDisplayZoomLevel = 1.000000 +ImmersiveMode = True +SustainedPerformanceMode = False +IgnoreScreenInsets = True +ReplaceTextures = True +SaveNewTextures = False +IgnoreTextureFilenames = False +TexScalingLevel = 1 +TexScalingType = 0 +TexDeposterize = False +TexHardwareScaling = False +VSyncInterval = False +BloomHack = 0 +SplineBezierQuality = 2 +HardwareTessellation = False +TextureShader = Off +ShaderChainRequires60FPS = False +MemBlockTransferGPU = True +DisableSlowFramebufEffects = False +FragmentTestCache = True +LogFrameDrops = False +InflightFrames = 2 +RenderDuplicateFrames = False +[Sound] +Enable = True +AudioBackend = 0 +ExtraAudioBuffering = False +GlobalVolume = 10 +ReverbVolume = 10 +AltSpeedVolume = -1 +AudioDevice = +AutoAudioDevice = False +[Control] +HapticFeedback = False +ShowTouchCross = True +ShowTouchCircle = True +ShowTouchSquare = True +ShowTouchTriangle = True +Custom0Mapping = 0x0000000000000000 +Custom0Image = 0 +Custom0Shape = 0 +Custom0Toggle = False +Custom1Mapping = 0x0000000000000000 +Custom1Image = 1 +Custom1Shape = 0 +Custom1Toggle = False +Custom2Mapping = 0x0000000000000000 +Custom2Image = 2 +Custom2Shape = 0 +Custom2Toggle = False +Custom3Mapping = 0x0000000000000000 +Custom3Image = 3 +Custom3Shape = 0 +Custom3Toggle = False +Custom4Mapping = 0x0000000000000000 +Custom4Image = 4 +Custom4Shape = 0 +Custom4Toggle = False +Custom5Mapping = 0x0000000000000000 +Custom5Image = 0 +Custom5Shape = 1 +Custom5Toggle = False +Custom6Mapping = 0x0000000000000000 +Custom6Image = 1 +Custom6Shape = 1 +Custom6Toggle = False +Custom7Mapping = 0x0000000000000000 +Custom7Image = 2 +Custom7Shape = 1 +Custom7Toggle = False +Custom8Mapping = 0x0000000000000000 +Custom8Image = 3 +Custom8Shape = 1 +Custom8Toggle = False +Custom9Mapping = 0x0000000000000000 +Custom9Image = 4 +Custom9Shape = 1 +Custom9Toggle = False +ShowTouchPause = False +ShowTouchControls = False +DisableDpadDiagonals = False +GamepadOnlyFocused = False +TouchButtonStyle = 1 +TouchButtonOpacity = 65 +TouchButtonHideSeconds = 20 +AutoCenterTouchAnalog = False +AnalogAutoRotSpeed = 8.000000 +TouchSnapToGrid = False +TouchSnapGridSize = 64 +ActionButtonSpacing2 = 1.000000 +ActionButtonCenterX = -1.000000 +ActionButtonCenterY = -1.000000 +ActionButtonScale = 1.150000 +DPadX = -1.000000 +DPadY = -1.000000 +DPadScale = 1.150000 +ShowTouchDpad = True +DPadSpacing = 1.000000 +StartKeyX = -1.000000 +StartKeyY = -1.000000 +StartKeyScale = 1.150000 +ShowTouchStart = True +SelectKeyX = -1.000000 +SelectKeyY = -1.000000 +SelectKeyScale = 1.150000 +ShowTouchSelect = True +UnthrottleKeyX = -1.000000 +UnthrottleKeyY = -1.000000 +UnthrottleKeyScale = 1.150000 +ShowTouchUnthrottle = True +LKeyX = -1.000000 +LKeyY = -1.000000 +LKeyScale = 1.150000 +ShowTouchLTrigger = True +RKeyX = -1.000000 +RKeyY = -1.000000 +RKeyScale = 1.150000 +ShowTouchRTrigger = True +AnalogStickX = -1.000000 +AnalogStickY = -1.000000 +AnalogStickScale = 1.150000 +ShowAnalogStick = True +RightAnalogStickX = -1.000000 +RightAnalogStickY = -1.000000 +RightAnalogStickScale = 1.150000 +ShowRightAnalogStick = False +fcombo0X = -1.000000 +fcombo0Y = -1.000000 +comboKeyScale0 = 1.150000 +ShowComboKey0 = False +fcombo1X = -1.000000 +fcombo1Y = -1.000000 +comboKeyScale1 = 1.150000 +ShowComboKey1 = False +fcombo2X = -1.000000 +fcombo2Y = -1.000000 +comboKeyScale2 = 1.150000 +ShowComboKey2 = False +fcombo3X = -1.000000 +fcombo3Y = -1.000000 +comboKeyScale3 = 1.150000 +ShowComboKey3 = False +fcombo4X = -1.000000 +fcombo4Y = -1.000000 +comboKeyScale4 = 1.150000 +ShowComboKey4 = False +fcombo5X = -1.000000 +fcombo5Y = -1.000000 +comboKeyScale5 = 1.150000 +ShowComboKey5 = False +fcombo6X = -1.000000 +fcombo6Y = -1.000000 +comboKeyScale6 = 1.150000 +ShowComboKey6 = False +fcombo7X = -1.000000 +fcombo7Y = -1.000000 +comboKeyScale7 = 1.150000 +ShowComboKey7 = False +fcombo8X = -1.000000 +fcombo8Y = -1.000000 +comboKeyScale8 = 1.150000 +ShowComboKey8 = False +fcombo9X = -1.000000 +fcombo9Y = -1.000000 +comboKeyScale9 = 1.150000 +ShowComboKey9 = False +AnalogDeadzone = 0.150000 +AnalogInverseDeadzone = 0.000000 +AnalogSensitivity = 1.100000 +AnalogIsCircular = False +AnalogLimiterDeadzone = 0.600000 +LeftStickHeadScale = 1.000000 +RightStickHeadScale = 1.000000 +HideStickBackground = False +UseMouse = False +MapMouse = False +ConfineMap = False +MouseSensitivity = 0.100000 +MouseSmoothing = 0.900000 +SystemControls = True +AllowMappingCombos = True +[Network] +EnableWlan = False +EnableAdhocServer = False +proAdhocServer = socom.cc +PortOffset = 10000 +MinTimeout = 0 +ForcedFirstConnect = False +EnableUPnP = False +UPnPUseOriginalPort = False +EnableNetworkChat = False +ChatButtonPosition = 0 +ChatScreenPosition = 0 +EnableQuickChat = True +QuickChat1 = Quick Chat 1 +QuickChat2 = Quick Chat 2 +QuickChat3 = Quick Chat 3 +QuickChat4 = Quick Chat 4 +QuickChat5 = Quick Chat 5 +[SystemParam] +PSPModel = 1 +PSPFirmwareVersion = 660 +NickName = PPSSPP +MacAddress = ec:fd:62:d4:ec:73 +Language = 1 +ParamTimeFormat = 0 +ParamDateFormat = 0 +TimeZone = 0 +DayLightSavings = False +ButtonPreference = 1 +LockParentalLevel = 0 +WlanAdhocChannel = 0 +WlanPowerSave = False +EncryptSave = True +SavedataUpgradeVersion = True +MemStickSize = 16 +[Debugger] +DisasmWindowX = -1 +DisasmWindowY = -1 +DisasmWindowW = -1 +DisasmWindowH = -1 +GEWindowX = -1 +GEWindowY = -1 +GEWindowW = -1 +GEWindowH = -1 +ConsoleWindowX = -1 +ConsoleWindowY = -1 +FontWidth = 8 +FontHeight = 12 +DisplayStatusBar = True +ShowBottomTabTitles = True +ShowDeveloperMenu = False +SkipDeadbeefFilling = False +FuncHashMap = False +MemInfoDetailed = False +DrawFrameGraph = False +[Upgrade] +UpgradeMessage = +UpgradeVersion = +DismissedVersion = +[Theme] +ItemStyleFg = 0xffffffff +ItemStyleBg = 0x55000000 +ItemFocusedStyleFg = 0xffffffff +ItemFocusedStyleBg = 0xffedc24c +ItemDownStyleFg = 0xffffffff +ItemDownStyleBg = 0xffbd9939 +ItemDisabledStyleFg = 0x80eeeeee +ItemDisabledStyleBg = 0x55e0d4af +ItemHighlightedStyleFg = 0xffffffff +ItemHighlightedStyleBg = 0x55bdbb39 +ButtonStyleFg = 0xffffffff +ButtonStyleBg = 0x55000000 +ButtonFocusedStyleFg = 0xffffffff +ButtonFocusedStyleBg = 0xffedc24c +ButtonDownStyleFg = 0xffffffff +ButtonDownStyleBg = 0xffbd9939 +ButtonDisabledStyleFg = 0x80eeeeee +ButtonDisabledStyleBg = 0x55e0d4af +ButtonHighlightedStyleFg = 0xffffffff +ButtonHighlightedStyleBg = 0x55bdbb39 +HeaderStyleFg = 0xffffffff +InfoStyleFg = 0xffffffff +InfoStyleBg = 0x00000000 +PopupTitleStyleFg = 0xffe3be59 +PopupStyleFg = 0xffffffff +PopupStyleBg = 0xff303030 +[Recent] +MaxRecent = 60 +[Log] +SYSTEMEnabled = True +SYSTEMLevel = 2 +BOOTEnabled = True +BOOTLevel = 2 +COMMONEnabled = True +COMMONLevel = 2 +CPUEnabled = True +CPULevel = 2 +FILESYSEnabled = True +FILESYSLevel = 2 +G3DEnabled = True +G3DLevel = 2 +HLEEnabled = True +HLELevel = 2 +JITEnabled = True +JITLevel = 2 +LOADEREnabled = True +LOADERLevel = 2 +MEEnabled = True +MELevel = 2 +MEMMAPEnabled = True +MEMMAPLevel = 2 +SASMIXEnabled = True +SASMIXLevel = 2 +SAVESTATEEnabled = True +SAVESTATELevel = 2 +FRAMEBUFEnabled = True +FRAMEBUFLevel = 2 +AUDIOEnabled = True +AUDIOLevel = 2 +IOEnabled = True +IOLevel = 2 +SCEAUDIOEnabled = True +SCEAUDIOLevel = 2 +SCECTRLEnabled = True +SCECTRLLevel = 2 +SCEDISPEnabled = True +SCEDISPLevel = 2 +SCEFONTEnabled = True +SCEFONTLevel = 2 +SCEGEEnabled = True +SCEGELevel = 2 +SCEINTCEnabled = True +SCEINTCLevel = 2 +SCEIOEnabled = True +SCEIOLevel = 2 +SCEKERNELEnabled = True +SCEKERNELLevel = 2 +SCEMODULEEnabled = True +SCEMODULELevel = 2 +SCENETEnabled = True +SCENETLevel = 2 +SCERTCEnabled = True +SCERTCLevel = 2 +SCESASEnabled = True +SCESASLevel = 2 +SCEUTILEnabled = True +SCEUTILLevel = 2 +SCEMISCEnabled = True +SCEMISCLevel = 2 +ACHIEVEMENTSEnabled = True +ACHIEVEMENTSLevel = 2 +HTTPEnabled = True +HTTPLevel = 2 +PRINTFEnabled = True +PRINTFLevel = 2 +[PostShaderSetting] +BloomSettingValue1 = 0.600000 +BloomSettingValue2 = 0.500000 +CartoonSettingValue1 = 0.500000 +ColorCorrectionSettingValue1 = 1.000000 +ColorCorrectionSettingValue2 = 1.000000 +ColorCorrectionSettingValue3 = 1.000000 +ColorCorrectionSettingValue4 = 1.000000 +ScanlinesSettingValue1 = 1.000000 +ScanlinesSettingValue2 = 0.500000 +SharpenSettingValue1 = 1.500000 +[Achievements] +AchievementsEnable = False +AchievementsChallengeMode = False +AchievementsEncoreMode = False +AchievementsUnofficial = False +AchievementsLogBadMemReads = False +AchievementsUserName = +AchievementsSoundEffects = True +AchievementsUnlockAudioFile = +AchievementsLeaderboardSubmitAudioFile = +AchievementsLeaderboardTrackerPos = 3 +AchievementsLeaderboardStartedOrFailedPos = 3 +AchievementsLeaderboardSubmittedPos = 3 +AchievementsProgressPos = 3 +AchievementsChallengePos = 3 +AchievementsUnlockedPos = 4 \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json new file mode 100644 index 0000000..3801e34 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json @@ -0,0 +1,15 @@ +{ + "name": "com.simeonradivoev.gameflow.ppsspp", + "displayName": "PPSSPP Integration", + "version": "0.0.1", + "description": "PPSSPP Emulator Integration", + "main": "./ppsspp.ts", + "icon": "https://www.ppsspp.org/static/img/platform/ppsspp-icon.png", + "category": "emulators", + "keywords": [ + "integration", + "emulator", + "psp", + "ppsspp" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts new file mode 100644 index 0000000..f69fdaf --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts @@ -0,0 +1,147 @@ +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import { config } from "@/bun/api/app"; +import configFilePathWin32 from './win32/ppsspp.ini' with { type: 'file' }; +import configControlsFilePathWin32 from './win32/controls.ini' with { type: 'file' }; +import configFilePathLinux from './linux/ppsspp.ini' with { type: 'file' }; +import configControlsFilePathLinux from './linux/controls.ini' with { type: 'file' }; +import path from "node:path"; +import Mustache from "mustache"; +import { ensureDir } from "fs-extra"; +import { homedir } from "node:os"; +import ini from 'ini'; +import fs from 'node:fs/promises'; +import { EmulatorCapabilities } from "@simeonradivoev/gameflow-sdk/shared"; + +export default class PPSSPPIntegration implements PluginType +{ + emulator = "PPSSPP"; + + async load (ctx: PluginLoadingContextType) + { + ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => + { + const stat = await fs.stat(ctx.path); + if (stat.isDirectory()) + { + await Bun.write(path.join(ctx.path, "portable.txt"), ""); + if (process.platform === 'win32') + { + await Bun.write(path.join(ctx.path, "installed.txt"), path.join(config.get('downloadPath'), 'saves', this.emulator)); + } + } + }); + + ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => + { + const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen"]; + + if (ctx.source?.type === 'store') + { + return { + id: desc.name, + supportLevel: "full", + capabilities: [...baseCapabilities, "config", "resolution"] + }; + } + else + { + return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] }; + } + }); + + ctx.hooks.games.postPlay.tapPromise({ name: desc.name }, async ({ saveFolderSlots, validChangedSaveFiles, command }) => + { + if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; + validChangedSaveFiles[this.emulator] = { + cwd: saveFolderSlots[this.emulator].cwd, + shared: true, + subPath: '*.{SFO,sfo,PNG,png}', + isGlob: true + }; + }); + + ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => + { + const args: string[] = []; + if (ctx.autoValidCommand.metadata.romPath) + { + args.push(ctx.autoValidCommand.metadata.romPath); + } + + args.push("--escape-exit", "--pause-menu-exit"); + if (config.get('launchInFullscreen')) + { + args.push("--fullscreen"); + } + + if (ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir && !ctx.dryRun) + { + let defaultConfigPath: string | undefined = undefined; + let defaultControlsPath: string | undefined = undefined; + + switch (process.platform) + { + case "win32": + defaultConfigPath = configFilePathWin32; + defaultControlsPath = configControlsFilePathWin32; + break; + case 'linux': + defaultConfigPath = configFilePathLinux; + defaultControlsPath = configControlsFilePathLinux; + break; + } + + let ppssppPath = ''; + if (process.platform === 'win32') + { + ppssppPath = path.join(config.get('downloadPath'), 'saves', this.emulator, 'PSP', 'SYSTEM'); + } else + { + //TODO: Use way to set custom memstick path when they support it + ensureDir(path.join(homedir(), '.config', 'ppsspp')); + ppssppPath = path.join(homedir(), '.config', 'ppsspp', 'PSP', 'SYSTEM'); + } + + ensureDir(ppssppPath); + + if (defaultConfigPath) + { + const resolutionMapping: Record = { + "720p": 2, + "1080p": 4, + "1440p": 6, + "4k": 8 + }; + const configPath = path.join(ppssppPath, 'ppsspp.ini'); + const configFile = Bun.file(configPath); + + const ppssppConfig = await configFile.exists() ? ini.parse(await configFile.text()) : ini.parse(await Bun.file(defaultConfigPath).text()); + + ppssppConfig.Graphics ??= {}; + ppssppConfig.Graphics.InternalResolution = resolutionMapping[config.get('emulatorResolution')] ?? 0; + ppssppConfig.Graphics.FullScreen = config.get('launchInFullscreen'); + + await Bun.write(configPath, ini.stringify(ppssppConfig)); + } + + if (defaultControlsPath) + { + const controlsFileContents = await Bun.file(defaultControlsPath).text(); + await Bun.write(path.join(ppssppPath, 'controls.ini'), Mustache.render(controlsFileContents, {})); + } + + return { + args, + savesPath: { + [this.emulator]: { + cwd: path.join(config.get('downloadPath'), 'saves', this.emulator, "PSP", "SAVEDATA") + } + } + }; + } + + return { args }; + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/controls.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/controls.ini new file mode 100644 index 0000000..54afdfa --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/controls.ini @@ -0,0 +1,23 @@ +[ControlMapping] +Up = 20-19 +Down = 20-20 +Left = 20-21 +Right = 20-22 +Circle = 20-97 +Cross = 20-96 +Square = 20-100 +Triangle = 20-99 +Start = 20-108 +Select = 20-109 +L = 20-102 +R = 20-103 +An.Up = 20-4002 +An.Down = 20-4003 +An.Left = 20-4001 +An.Right = 20-4000 +Fast-forward = 20-109:20-4036 +Rewind = 20-109:20-4034 +Save State = 20-109:20-103 +Load State = 20-109:20-102 +Home = 20-108:20-109 +Exit App = 20-3:20-108 \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini new file mode 100644 index 0000000..f448165 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini @@ -0,0 +1,470 @@ +[General] +FirstRun = False +RunCount = 4 +Enable Logging = True +AutoRun = True +Browse = False +IgnoreBadMemAccess = True +CurrentDirectory = C:/Emulation/roms/psp +ShowDebuggerOnLoad = False +CheckForNewVersion = True +Language = en_US +ForceLagSync2 = False +DiscordPresence = True +UISound = False +AutoLoadSaveState = 0 +EnableCheats = True +CwCheatRefreshRate = 77 +CwCheatScrollPosition = 0.000000 +GameListScrollPosition = 0.000000 +ScreenshotsAsPNG = False +UseFFV1 = False +DumpFrames = False +DumpVideoOutput = False +DumpAudio = False +SaveLoadResetsAVdumping = False +StateSlot = 0 +EnableStateUndo = True +StateLoadUndoGame = NA +StateUndoLastSaveGame = NA +StateUndoLastSaveSlot = -5 +RewindFlipFrequency = 0 +ShowOnScreenMessage = True +ShowRegionOnGameIcon = False +ShowIDOnGameIcon = False +GameGridScale = 1.000000 +GridView1 = True +GridView2 = True +GridView3 = False +RightAnalogUp = 0 +RightAnalogDown = 0 +RightAnalogLeft = 0 +RightAnalogRight = 0 +RightAnalogPress = 0 +RightAnalogCustom = False +RightAnalogDisableDiagonal = False +SwipeUp = 0 +SwipeDown = 0 +SwipeLeft = 0 +SwipeRight = 0 +SwipeSensitivity = 1.000000 +SwipeSmoothing = 0.300000 +DoubleTapGesture = 0 +GestureControlEnabled = False +ReportingHost = default +AutoSaveSymbolMap = False +CacheFullIsoInRam = False +RemoteISOPort = 0 +LastRemoteISOServer = +LastRemoteISOPort = 0 +RemoteISOManualConfig = False +RemoteShareOnStartup = False +RemoteISOSubdir = / +RemoteDebuggerOnStartup = False +InternalScreenRotation = 1 +BackgroundAnimation = 1 +PauseWhenMinimized = False +DumpDecryptedEboots = False +MemStickInserted = True +EnablePlugins = True +[CPU] +CPUCore = 1 +SeparateSASThread = True +SeparateIOThread = True +IOTimingMethod = 0 +FastMemoryAccess = True +FunctionReplacements = True +HideSlowWarnings = False +HideStateWarnings = False +PreloadFunctions = False +JitDisableFlags = 0x00000000 +CPUSpeed = 0 +[Graphics] +EnableCardboardVR = False +CardboardScreenSize = 50 +CardboardXShift = 0 +CardboardYShift = 0 +ShowFPSCounter = 0 +GraphicsBackend = 3 (VULKAN) +FailedGraphicsBackends = +DisabledGraphicsBackends = +VulkanDevice = +CameraDevice = +RenderingMode = 1 +SoftwareRenderer = False +HardwareTransform = True +SoftwareSkinning = True +TextureFiltering = 1 +BufferFiltering = 1 +AndroidHwScale = 1 +HighQualityDepth = 1 +FrameSkip = 0 +FrameSkipType = 0 +AutoFrameSkip = False +FrameRate = 0 +FrameRate2 = -1 +UnthrottlingMode = CONTINUOUS +AnisotropyLevel = 4 +VertexDecCache = False +TextureBackoffCache = False +TextureSecondaryCache = False +FullScreenMulti = False +SmallDisplayZoomType = 2 +SmallDisplayOffsetX = 0.500000 +SmallDisplayOffsetY = 0.500000 +SmallDisplayZoomLevel = 1.000000 +ImmersiveMode = True +SustainedPerformanceMode = False +IgnoreScreenInsets = True +ReplaceTextures = True +SaveNewTextures = False +IgnoreTextureFilenames = False +TexScalingLevel = 1 +TexScalingType = 0 +TexDeposterize = False +TexHardwareScaling = False +VSyncInterval = False +BloomHack = 0 +SplineBezierQuality = 2 +HardwareTessellation = False +TextureShader = Off +ShaderChainRequires60FPS = False +MemBlockTransferGPU = True +DisableSlowFramebufEffects = False +FragmentTestCache = True +LogFrameDrops = False +InflightFrames = 2 +RenderDuplicateFrames = False +[Sound] +Enable = True +AudioBackend = 0 +ExtraAudioBuffering = False +GlobalVolume = 10 +ReverbVolume = 10 +AltSpeedVolume = -1 +AudioDevice = +AutoAudioDevice = False +[Control] +HapticFeedback = False +ShowTouchCross = True +ShowTouchCircle = True +ShowTouchSquare = True +ShowTouchTriangle = True +Custom0Mapping = 0x0000000000000000 +Custom0Image = 0 +Custom0Shape = 0 +Custom0Toggle = False +Custom1Mapping = 0x0000000000000000 +Custom1Image = 1 +Custom1Shape = 0 +Custom1Toggle = False +Custom2Mapping = 0x0000000000000000 +Custom2Image = 2 +Custom2Shape = 0 +Custom2Toggle = False +Custom3Mapping = 0x0000000000000000 +Custom3Image = 3 +Custom3Shape = 0 +Custom3Toggle = False +Custom4Mapping = 0x0000000000000000 +Custom4Image = 4 +Custom4Shape = 0 +Custom4Toggle = False +Custom5Mapping = 0x0000000000000000 +Custom5Image = 0 +Custom5Shape = 1 +Custom5Toggle = False +Custom6Mapping = 0x0000000000000000 +Custom6Image = 1 +Custom6Shape = 1 +Custom6Toggle = False +Custom7Mapping = 0x0000000000000000 +Custom7Image = 2 +Custom7Shape = 1 +Custom7Toggle = False +Custom8Mapping = 0x0000000000000000 +Custom8Image = 3 +Custom8Shape = 1 +Custom8Toggle = False +Custom9Mapping = 0x0000000000000000 +Custom9Image = 4 +Custom9Shape = 1 +Custom9Toggle = False +ShowTouchPause = False +ShowTouchControls = False +DisableDpadDiagonals = False +GamepadOnlyFocused = False +TouchButtonStyle = 1 +TouchButtonOpacity = 65 +TouchButtonHideSeconds = 20 +AutoCenterTouchAnalog = False +AnalogAutoRotSpeed = 8.000000 +TouchSnapToGrid = False +TouchSnapGridSize = 64 +ActionButtonSpacing2 = 1.000000 +ActionButtonCenterX = -1.000000 +ActionButtonCenterY = -1.000000 +ActionButtonScale = 1.150000 +DPadX = -1.000000 +DPadY = -1.000000 +DPadScale = 1.150000 +ShowTouchDpad = True +DPadSpacing = 1.000000 +StartKeyX = -1.000000 +StartKeyY = -1.000000 +StartKeyScale = 1.150000 +ShowTouchStart = True +SelectKeyX = -1.000000 +SelectKeyY = -1.000000 +SelectKeyScale = 1.150000 +ShowTouchSelect = True +UnthrottleKeyX = -1.000000 +UnthrottleKeyY = -1.000000 +UnthrottleKeyScale = 1.150000 +ShowTouchUnthrottle = True +LKeyX = -1.000000 +LKeyY = -1.000000 +LKeyScale = 1.150000 +ShowTouchLTrigger = True +RKeyX = -1.000000 +RKeyY = -1.000000 +RKeyScale = 1.150000 +ShowTouchRTrigger = True +AnalogStickX = -1.000000 +AnalogStickY = -1.000000 +AnalogStickScale = 1.150000 +ShowAnalogStick = True +RightAnalogStickX = -1.000000 +RightAnalogStickY = -1.000000 +RightAnalogStickScale = 1.150000 +ShowRightAnalogStick = False +fcombo0X = -1.000000 +fcombo0Y = -1.000000 +comboKeyScale0 = 1.150000 +ShowComboKey0 = False +fcombo1X = -1.000000 +fcombo1Y = -1.000000 +comboKeyScale1 = 1.150000 +ShowComboKey1 = False +fcombo2X = -1.000000 +fcombo2Y = -1.000000 +comboKeyScale2 = 1.150000 +ShowComboKey2 = False +fcombo3X = -1.000000 +fcombo3Y = -1.000000 +comboKeyScale3 = 1.150000 +ShowComboKey3 = False +fcombo4X = -1.000000 +fcombo4Y = -1.000000 +comboKeyScale4 = 1.150000 +ShowComboKey4 = False +fcombo5X = -1.000000 +fcombo5Y = -1.000000 +comboKeyScale5 = 1.150000 +ShowComboKey5 = False +fcombo6X = -1.000000 +fcombo6Y = -1.000000 +comboKeyScale6 = 1.150000 +ShowComboKey6 = False +fcombo7X = -1.000000 +fcombo7Y = -1.000000 +comboKeyScale7 = 1.150000 +ShowComboKey7 = False +fcombo8X = -1.000000 +fcombo8Y = -1.000000 +comboKeyScale8 = 1.150000 +ShowComboKey8 = False +fcombo9X = -1.000000 +fcombo9Y = -1.000000 +comboKeyScale9 = 1.150000 +ShowComboKey9 = False +AnalogDeadzone = 0.150000 +AnalogInverseDeadzone = 0.000000 +AnalogSensitivity = 1.100000 +AnalogIsCircular = False +AnalogLimiterDeadzone = 0.600000 +LeftStickHeadScale = 1.000000 +RightStickHeadScale = 1.000000 +HideStickBackground = False +UseMouse = False +MapMouse = False +ConfineMap = False +MouseSensitivity = 0.100000 +MouseSmoothing = 0.900000 +SystemControls = True +AllowMappingCombos = True +[Network] +EnableWlan = False +EnableAdhocServer = False +proAdhocServer = socom.cc +PortOffset = 10000 +MinTimeout = 0 +ForcedFirstConnect = False +EnableUPnP = False +UPnPUseOriginalPort = False +EnableNetworkChat = False +ChatButtonPosition = 0 +ChatScreenPosition = 0 +EnableQuickChat = True +QuickChat1 = Quick Chat 1 +QuickChat2 = Quick Chat 2 +QuickChat3 = Quick Chat 3 +QuickChat4 = Quick Chat 4 +QuickChat5 = Quick Chat 5 +[SystemParam] +PSPModel = 1 +PSPFirmwareVersion = 660 +NickName = PPSSPP +MacAddress = ec:fd:62:d4:ec:73 +Language = 1 +ParamTimeFormat = 0 +ParamDateFormat = 0 +TimeZone = 0 +DayLightSavings = False +ButtonPreference = 1 +LockParentalLevel = 0 +WlanAdhocChannel = 0 +WlanPowerSave = False +EncryptSave = True +SavedataUpgradeVersion = True +MemStickSize = 16 +[Debugger] +DisasmWindowX = -1 +DisasmWindowY = -1 +DisasmWindowW = -1 +DisasmWindowH = -1 +GEWindowX = -1 +GEWindowY = -1 +GEWindowW = -1 +GEWindowH = -1 +ConsoleWindowX = -1 +ConsoleWindowY = -1 +FontWidth = 8 +FontHeight = 12 +DisplayStatusBar = True +ShowBottomTabTitles = True +ShowDeveloperMenu = False +SkipDeadbeefFilling = False +FuncHashMap = False +MemInfoDetailed = False +DrawFrameGraph = False +[Upgrade] +UpgradeMessage = +UpgradeVersion = +DismissedVersion = +[Theme] +ItemStyleFg = 0xffffffff +ItemStyleBg = 0x55000000 +ItemFocusedStyleFg = 0xffffffff +ItemFocusedStyleBg = 0xffedc24c +ItemDownStyleFg = 0xffffffff +ItemDownStyleBg = 0xffbd9939 +ItemDisabledStyleFg = 0x80eeeeee +ItemDisabledStyleBg = 0x55e0d4af +ItemHighlightedStyleFg = 0xffffffff +ItemHighlightedStyleBg = 0x55bdbb39 +ButtonStyleFg = 0xffffffff +ButtonStyleBg = 0x55000000 +ButtonFocusedStyleFg = 0xffffffff +ButtonFocusedStyleBg = 0xffedc24c +ButtonDownStyleFg = 0xffffffff +ButtonDownStyleBg = 0xffbd9939 +ButtonDisabledStyleFg = 0x80eeeeee +ButtonDisabledStyleBg = 0x55e0d4af +ButtonHighlightedStyleFg = 0xffffffff +ButtonHighlightedStyleBg = 0x55bdbb39 +HeaderStyleFg = 0xffffffff +InfoStyleFg = 0xffffffff +InfoStyleBg = 0x00000000 +PopupTitleStyleFg = 0xffe3be59 +PopupStyleFg = 0xffffffff +PopupStyleBg = 0xff303030 +[Recent] +MaxRecent = 60 +[Log] +SYSTEMEnabled = True +SYSTEMLevel = 2 +BOOTEnabled = True +BOOTLevel = 2 +COMMONEnabled = True +COMMONLevel = 2 +CPUEnabled = True +CPULevel = 2 +FILESYSEnabled = True +FILESYSLevel = 2 +G3DEnabled = True +G3DLevel = 2 +HLEEnabled = True +HLELevel = 2 +JITEnabled = True +JITLevel = 2 +LOADEREnabled = True +LOADERLevel = 2 +MEEnabled = True +MELevel = 2 +MEMMAPEnabled = True +MEMMAPLevel = 2 +SASMIXEnabled = True +SASMIXLevel = 2 +SAVESTATEEnabled = True +SAVESTATELevel = 2 +FRAMEBUFEnabled = True +FRAMEBUFLevel = 2 +AUDIOEnabled = True +AUDIOLevel = 2 +IOEnabled = True +IOLevel = 2 +SCEAUDIOEnabled = True +SCEAUDIOLevel = 2 +SCECTRLEnabled = True +SCECTRLLevel = 2 +SCEDISPEnabled = True +SCEDISPLevel = 2 +SCEFONTEnabled = True +SCEFONTLevel = 2 +SCEGEEnabled = True +SCEGELevel = 2 +SCEINTCEnabled = True +SCEINTCLevel = 2 +SCEIOEnabled = True +SCEIOLevel = 2 +SCEKERNELEnabled = True +SCEKERNELLevel = 2 +SCEMODULEEnabled = True +SCEMODULELevel = 2 +SCENETEnabled = True +SCENETLevel = 2 +SCERTCEnabled = True +SCERTCLevel = 2 +SCESASEnabled = True +SCESASLevel = 2 +SCEUTILEnabled = True +SCEUTILLevel = 2 +SCEMISCEnabled = True +SCEMISCLevel = 2 +[PostShaderSetting] +BloomSettingValue1 = 0.600000 +BloomSettingValue2 = 0.500000 +CartoonSettingValue1 = 0.500000 +ColorCorrectionSettingValue1 = 1.000000 +ColorCorrectionSettingValue2 = 1.000000 +ColorCorrectionSettingValue3 = 1.000000 +ColorCorrectionSettingValue4 = 1.000000 +ScanlinesSettingValue1 = 1.000000 +ScanlinesSettingValue2 = 0.500000 +SharpenSettingValue1 = 1.500000 +[Achievements] +AchievementsEnable = False +AchievementsChallengeMode = False +AchievementsEncoreMode = False +AchievementsUnofficial = False +AchievementsLogBadMemReads = False +AchievementsSoundEffects = True +AchievementsUnlockAudioFile = +AchievementsLeaderboardSubmitAudioFile = +AchievementsLeaderboardTrackerPos = 3 +AchievementsLeaderboardStartedOrFailedPos = 3 +AchievementsLeaderboardSubmittedPos = 3 +AchievementsProgressPos = 3 +AchievementsChallengePos = 3 +AchievementsUnlockedPos = 4 \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/eeprom.bin b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/eeprom.bin new file mode 100644 index 0000000..55874b0 Binary files /dev/null and b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/eeprom.bin differ diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json new file mode 100644 index 0000000..937ebc3 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json @@ -0,0 +1,15 @@ +{ + "name": "com.simeonradivoev.gameflow.xemu", + "displayName": "XEMU Integration", + "version": "0.0.1", + "description": "XEMU Emulator Integration", + "main": "./xemu.ts", + "icon": "https://upload.wikimedia.org/wikipedia/commons/8/8e/Xemu_logo_green.svg", + "category": "emulators", + "keywords": [ + "integration", + "emulator", + "xbox", + "xemu" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu.ts new file mode 100644 index 0000000..57506de --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu.ts @@ -0,0 +1,74 @@ +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import { config } from "@/bun/api/app"; +import path from "node:path"; +import toml, { TomlTable } from 'smol-toml'; +import fs from 'node:fs/promises'; +import bin from './eeprom.bin' with { type: 'file' }; + +export default class XEMUIntegration implements PluginType +{ + emulator = 'XEMU'; + + async load (ctx: PluginLoadingContextType) + { + ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => + { + return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen"] }; + }); + + ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => + { + const args: string[] = []; + + if (config.get('launchInFullscreen')) + { + args.push("-full-screen"); + } + + if (ctx.autoValidCommand.metadata.romPath) + { + args.push("-dvd_path"); + args.push(ctx.autoValidCommand.metadata.romPath); + } + + const configPath = path.join(config.get('downloadPath'), 'storage', this.emulator, 'xemu.toml'); + let configFile: { general: TomlTable & { misc: TomlTable; }, sys: TomlTable & { files: TomlTable; }; } = { general: { misc: {} }, sys: { files: {} } }; + if (await Bun.file(configPath).exists()) + { + configFile = toml.parse(await Bun.file(configPath).text()) as any; + } + + configFile.general.misc ??= {}; + configFile.general.misc.skip_boot_anim = true; + configFile.general.show_welcome = false; + configFile.general.games_dir = path.join(config.get('downloadPath'), 'roms', 'xbox'); + configFile.sys.mem_limit = '128'; + const biosFolder = path.join(config.get('downloadPath'), "bios", this.emulator); + if (await fs.exists(biosFolder)) + { + const biosPaths = (await fs.readdir(biosFolder)); + const flash = biosPaths.find(f => f.endsWith('.bin') && !f.includes('mcpx')); + const bootrom = biosPaths.find(f => f.endsWith('.bin') && f.includes('mcpx')); + const hardDrive = biosPaths.find(f => f.endsWith('qcow2')); + if (flash) configFile.sys.files.flashrom_path = path.join(biosFolder, flash); + if (bootrom) configFile.sys.files.bootrom_path = path.join(biosFolder, bootrom); + if (hardDrive) configFile.sys.files.hdd_path = path.join(biosFolder, hardDrive); + } + + if (!ctx.dryRun) + { + const eepromPath = path.join(config.get('downloadPath'), "storage", this.emulator, 'eeprom.bin'); + await Bun.write(eepromPath, await Bun.file(bin).arrayBuffer()); + configFile.sys.files.eeprom_path = eepromPath; + + await Bun.write(configPath, toml.stringify(configFile)); + args.push("-config_path"); + args.push(configPath); + } + + + return { args }; + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json new file mode 100644 index 0000000..280f14f --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json @@ -0,0 +1,16 @@ +{ + "name": "com.simeonradivoev.gameflow.xenia", + "displayName": "XENIA Integration", + "version": "0.0.1", + "description": "XENIA Emulator Integration", + "main": "./xenia.ts", + "icon": "https://xenia.jp/images/logo-256x256.png", + "category": "emulators", + "keywords": [ + "integration", + "emulator", + "xbox360", + "xenia", + "xenia-edge" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/utils.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/utils.ts new file mode 100644 index 0000000..b3c26f9 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/utils.ts @@ -0,0 +1,140 @@ +import { join } from "path"; + +const SECTOR_SIZE = 0x800; +const MAGIC = "MICROSOFT*XBOX*MEDIA"; + +const PARTITION_OFFSETS: Record = { + XSF: 0x0, + GDF: 0xFD90000, + XGD3: 0x2080000, +}; + +async function readBytes (file: ReturnType, offset: number, length: number): Promise +{ + return Buffer.from(await file.slice(offset, offset + length).arrayBuffer()); +} + +async function parseTitleIdFromXexReader ( + read: (offset: number, length: number) => Promise +): Promise +{ + // Read just the fixed header (magic + flags + offsets + header count) + const header = await read(0, 0x18); + if (header.toString("ascii", 0, 4) !== "XEX2") + { + throw new Error("Not a valid XEX2 file"); + } + + const headerCount = header.readUInt32BE(0x14); + const EXEC_INFO_KEY = 0x40006; + + // Read the optional header table + const table = await read(0x18, headerCount * 8); + + for (let i = 0; i < headerCount; i++) + { + const key = table.readUInt32BE(i * 8); + const valueOrOffset = table.readUInt32BE(i * 8 + 4); + + if (key === EXEC_INFO_KEY) + { + // valueOrOffset is a file offset — read the exec info struct there + // TitleID is at +0x0C within it + const execInfo = await read(valueOrOffset, 0x18); + return execInfo.readUInt32BE(0x0C) + .toString(16).toUpperCase().padStart(8, "0"); + } + } + + throw new Error("Execution info header not found in XEX"); +} + +async function titleIdFromXexFile (xexPath: string): Promise +{ + const file = Bun.file(xexPath); + return parseTitleIdFromXexReader((offset, length) => + readBytes(file, offset, length) + ); +} + +async function titleIdFromIso (isoPath: string): Promise +{ + const file = Bun.file(isoPath); + const fileSize = file.size; + + for (const partitionOffset of Object.values(PARTITION_OFFSETS)) + { + const vdOffset = partitionOffset + 0x20 * SECTOR_SIZE; + if (vdOffset + 28 > fileSize) continue; + + const vd = await readBytes(file, vdOffset, 28); + if (vd.toString("ascii", 0, 20) !== MAGIC) continue; + + const rootSector = vd.readUInt32LE(20); + const rootSize = vd.readUInt32LE(24); + const rootOffset = partitionOffset + rootSector * SECTOR_SIZE; + const dir = await readBytes(file, rootOffset, rootSize); + + let pos = 0; + while (pos < dir.length) + { + if (dir[pos] === 0xFF) break; + if (pos + 14 > dir.length) break; + + const nameLen = dir[pos + 13]; + if (nameLen === 0 || nameLen === 0xFF) break; + if (pos + 14 + nameLen > dir.length) break; + + const name = dir.toString("ascii", pos + 14, pos + 14 + nameLen); + const fileSector = dir.readUInt32LE(pos + 4); + + if (name.toLowerCase() === "default.xex") + { + const xexBase = partitionOffset + fileSector * SECTOR_SIZE; + // Reader that translates relative XEX offsets to absolute ISO offsets + return parseTitleIdFromXexReader((offset, length) => + readBytes(file, xexBase + offset, length) + ); + } + + const entryLen = 14 + nameLen; + pos += (entryLen + 3) & ~3; + } + } + + throw new Error("Not a valid Xbox 360 ISO or default.xex not found"); +} + +async function titleIdFromFolder (folderPath: string): Promise +{ + return titleIdFromXexFile(join(folderPath, "default.xex")); +} + +type XeniaRomType = "iso" | "xex" | "folder"; + +function detectRomType (romPath: string): XeniaRomType +{ + const lower = romPath.toLowerCase(); + if (lower.endsWith(".iso")) return "iso"; + if (lower.endsWith(".xex")) return "xex"; + return "folder"; // extracted game folder containing default.xex +} + +async function getTitleId (romPath: string): Promise +{ + switch (detectRomType(romPath)) + { + case "iso": return titleIdFromIso(romPath); + case "xex": return titleIdFromXexFile(romPath); + case "folder": return titleIdFromFolder(romPath); + } +} + +export async function getXeniaSavePaths ( + romPath: string, + xeniaDir: string +): Promise +{ + const titleId = await getTitleId(romPath); + return join(xeniaDir, titleId); +}; \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia.ts new file mode 100644 index 0000000..9d37d99 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia.ts @@ -0,0 +1,102 @@ +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import { GameflowHooks } from "@simeonradivoev/gameflow-sdk"; +import { config } from "@/bun/api/app"; +import path from "node:path"; +import { ensureDir } from "fs-extra"; +import toml, { TomlTable } from 'smol-toml'; +import fs from 'node:fs/promises'; +import { getXeniaSavePaths } from "./utils"; + +export default class XENIAIntegration implements PluginType +{ + emulator = 'XENIA'; + emulatorEdge = 'XENIA-EDGE'; + + async handlePostInstall (ctx: Parameters['0']) + { + await Bun.write(path.join(ctx.path, "portable.txt"), ""); + } + + async handleLaunch (ctx: Parameters['0']): + ReturnType + { + const args: string[] = []; + + if (ctx.autoValidCommand.metadata.romPath) + { + args.push(ctx.autoValidCommand.metadata.romPath); + } + + const configPath = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, `${ctx.autoValidCommand.emulator}.toml`); + + args.push(`--config`, configPath); + + if (config.get('launchInFullscreen')) + { + args.push(`--fullscreen`); + } + + if (!ctx.dryRun) + { + await ensureDir(path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!)); + let configFile: TomlTable & { Storage: TomlTable, GPU: TomlTable, Display: TomlTable; } = { Storage: {}, GPU: {}, Display: {} }; + if (await fs.exists(configPath)) + { + configFile = toml.parse(await Bun.file(configPath).text()) as any; + } + + const resolutionMapping = { + "720p": 1, + "1080p": 2, + "1440p": 3, + "4k": 3 + }; + + configFile.Display.fullscreen = config.get('launchInFullscreen'); + configFile.GPU.draw_resolution_scale_x = resolutionMapping[config.get('emulatorResolution')] ?? 1; + configFile.GPU.draw_resolution_scale_y = resolutionMapping[config.get('emulatorResolution')] ?? 1; + const savesPath = path.join(config.get('downloadPath'), 'saves', ctx.autoValidCommand.emulator!); + await ensureDir(savesPath); + configFile.Storage.content_root = path.join(config.get('downloadPath'), 'saves', ctx.autoValidCommand.emulator!); + configFile.Storage.storage_root = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, 'config'); + configFile.Storage.cache_root = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, 'cache'); + + await Bun.write(configPath, toml.stringify(configFile)); + + let finalSavesPath: string | undefined = undefined; + if (ctx.autoValidCommand.metadata.romPath) + { + finalSavesPath = await getXeniaSavePaths(ctx.autoValidCommand.metadata.romPath, savesPath); + return { args, savesPath: { [this.emulator]: { cwd: finalSavesPath } } }; + } + + return { args }; + }; + + return { args }; + } + + handleEmulatorLaunchSupport (ctx: Parameters['0']): + ReturnType + { + return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen", "saves"] }; + } + + async load (ctx: PluginLoadingContextType) + { + ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, this.handleEmulatorLaunchSupport); + ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulatorEdge }, this.handleEmulatorLaunchSupport); + + ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, this.handleLaunch); + ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulatorEdge }, this.handleLaunch); + + ctx.hooks.games.postPlay.tap({ name: desc.name }, async ({ validChangedSaveFiles, saveFolderSlots, command }) => + { + if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; + const files = await fs.readdir(saveFolderSlots[this.emulator].cwd, { recursive: true }); + validChangedSaveFiles.xenia = { cwd: saveFolderSlots[this.emulator].cwd, subPath: files, shared: false }; + + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/es-de.ts b/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/es-de.ts new file mode 100644 index 0000000..77cd201 --- /dev/null +++ b/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/es-de.ts @@ -0,0 +1,521 @@ +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import { config, customEmulators, db, emulatorsDb } from "@/bun/api/app"; +import * as emulatorSchema from '@schema/emulators'; +import { and, eq } from "drizzle-orm"; +import { cores } from "@/bun/api/emulatorjs/emulatorjs"; +import { RPC_URL } from "@/shared/constants"; +import { host } from "@/bun/utils/host"; +import path from 'node:path'; +import { existsSync, readFileSync } from "node:fs"; +import fs from "node:fs/promises"; +import { findStoreEmulatorExec } from "@/bun/api/games/services/launchGameService"; +import { which } from "bun"; +import os from 'node:os'; +import { getLocalGameMatch } from "@/bun/api/games/services/utils"; +import { CommandEntry, EmulatorSourceEntryType } from "@simeonradivoev/gameflow-sdk/shared"; + +export default class IgdbIntegration implements PluginType +{ + varRegex = /%([^%]+)%/g; + assignRegex = /(%\w+%)=(\S+) /g; + + /** + * Get the emulators related to the given system + * @param systemSlug the ES-DE slug for the system + */ + async getEmulatorsForSystem (systemSlug: string) + { + const system = await emulatorsDb.query.systems.findFirst({ + with: { commands: true }, + where: eq(emulatorSchema.systems.name, systemSlug) + }); + + if (!system) + { + throw new Error(`Could not find system '${systemSlug}'`); + } + + const emulators = new Set(); + await Promise.all(system.commands.map(async (command, index) => + { + let cmd = command.command; + + const matches = Array.from(cmd.matchAll(this.varRegex)); + matches.forEach(([value]) => + { + if (value.startsWith("%EMULATOR_")) + { + const emulatorName = value.substring("%EMULATOR_".length, value.length - 1); + emulators.add(emulatorName); + return; + } + }); + })); + + if (cores[systemSlug]) + { + emulators.add('EMULATORJS'); + } + + return Array.from(emulators); + } + + async findExecs (id: string, emulator?: { winregistrypath: string[], systempath: string[], staticpath: string[]; }) + { + const execs: EmulatorSourceEntryType[] = []; + + if (customEmulators.has(id)) + { + execs.push({ binPath: customEmulators.get(id), type: 'custom', exists: await fs.exists(customEmulators.get(id)) }); + } + + if (emulator && emulator.systempath.length > 0) + { + const storePath = await findStoreEmulatorExec(id, emulator); + if (storePath) execs.push(storePath); + } + + if (emulator && process.platform === 'win32') + { + const regValues = emulator.winregistrypath; + if (regValues.length > 0) + { + for (const node of regValues) + { + const registryValue = await this.readRegistryValue(node); + if (registryValue) + { + execs.push({ binPath: registryValue, type: 'registry', exists: true }); + } + } + + } + } + + if (emulator && emulator.systempath.length > 0) + { + const systemPath = await this.resolveSystemPath(emulator.systempath); + if (systemPath) + { + execs.push({ binPath: systemPath, type: 'system', exists: true }); + } + } + + if (emulator && emulator.staticpath.length > 0) + { + const staticPath = await this.resolveStaticPath(emulator.staticpath); + if (staticPath) + { + execs.push({ binPath: staticPath, type: 'static', exists: true }); + } + } + + return execs; + } + + async readRegistryValue (text: string) + { + const params = text.split('|'); + const key = path.dirname(params[0]); + const value = path.basename(params[0]); + const bin = params.length > 1 ? params[1] : undefined; + + const proc = Bun.spawn({ + cmd: ["reg", "QUERY", key, "/v", value], + stdout: "pipe", + stderr: "pipe", + }); + + const output = await new Response(proc.stdout).text(); + await proc.exited; + + if (!output.includes(value)) return null; + + const lines = output.split("\n"); + for (const line of lines) + { + if (line.includes(value)) + { + const parts = line.trim().split(/\s{4,}/); + return bin ? path.join(parts[2], bin) : parts[2]; // registry value + } + } + + return null; + } + + async resolveStaticPath (entries: string[]) + { + for (const entry of entries) + { + const resolved = entry.replace("~", os.homedir()); + if (await fs.exists(resolved)) + { + return resolved; + } + } + return null; + } + + async resolveSystemPath (entries: string[]) + { + for (const entry of entries) + { + try + { + const found = which(entry); + return found; + } catch { } + } + return null; + } + + async findExecsByName (emulatorName: string) + { + const emulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, emulatorName) }); + if (!emulator) + { + return []; + } + return this.findExecs(emulatorName, emulator); + } + + async getRomFilePaths (gamePath: string, config: { systemSlug?: string; mainGlob?: string | null; }) + { + if (!existsSync(gamePath)) + { + throw new Error(`Provided rom path is missing: '${gamePath}'`); + } + + const gamePathStat = await fs.stat(gamePath); + const validFiles: string[] = []; + + if (gamePathStat.isDirectory()) + { + if (config.mainGlob) + { + const files = await Array.fromAsync(fs.glob(config.mainGlob, { cwd: gamePath })); + if (files.length > 1) + { + throw new Error("Found multiple rom files"); + } else if (files.length === 0) + { + throw new Error("Found no valid roms"); + } + + validFiles.push(path.join(gamePath, files[0])); + } else + { + if (!config.systemSlug) throw new Error("Needs system to find valid file"); + + const system = await emulatorsDb.query.systems.findFirst({ + with: { commands: true }, + where: eq(emulatorSchema.systems.name, config.systemSlug) + }); + + if (!system) + { + throw new Error(`Could not find system '${config.systemSlug}'`); + } + + const extensionList = system.extension.join(','); + + for await (const file of fs.glob(path.join(gamePath, `/**/*.{${extensionList}}`))) + { + validFiles.push(file); + } + + if (validFiles.length <= 0) + { + throw new Error(`Could not find valid rom file. Must be a file that ends in '${extensionList}'`); + } + } + } else if (config.systemSlug) + { + const system = await emulatorsDb.query.systems.findFirst({ + with: { commands: true }, + where: eq(emulatorSchema.systems.name, config.systemSlug) + }); + + if (!system) + { + throw new Error(`Could not find system '${config.systemSlug}'`); + } + + if (system.extension.some(e => gamePath.toLocaleLowerCase().endsWith(e.toLocaleLowerCase()))) + { + validFiles.push(gamePath); + } + else + { + const extensionList = system.extension.join(','); + throw new Error(`Invalid Rom File. Must be a file that ends in '${extensionList}'`); + } + } else + { + validFiles.push(gamePath); + } + + return validFiles; + } + + /** + * + * @param data Uses es-de system slug + * @param mainGlob The main file glob supported pattern to search for if game path is a directory + * @returns + */ + async getValidLaunchCommands (data: { + systemSlug: string; + gamePath: string; + mainGlob?: string | null; + }): Promise + { + + const system = await emulatorsDb.query.systems.findFirst({ + with: { commands: true }, + where: eq(emulatorSchema.systems.name, data.systemSlug) + }); + + if (!system) + { + throw new Error(`Could not find system '${data.systemSlug}'`); + } + + if (!system.extension || system.extension.length <= 0) + { + throw new Error(`No extensions listed for system '${data.systemSlug}'`); + } + + const downloadPath = config.get('downloadPath'); + const gamePath = path.isAbsolute(data.gamePath) ? data.gamePath : path.join(downloadPath, data.gamePath); + + const validFiles: string[] = await this.getRomFilePaths(gamePath, { systemSlug: data.systemSlug, mainGlob: data.mainGlob }); + + function escapeWindowsArg (arg: string): string + { + if (process.platform === 'win32') + { + return `"${arg + .replace(/(\\*)"/g, '$1$1\\"') // escape quotes + .replace(/(\\*)$/, '$1$1') // escape trailing backslashes + }"`; + } else + { + if (arg.includes(' ')) + { + return `"${arg}"`; + } else + { + return arg; + } + } + } + + const formattedCommands = await Promise.all(system.commands + .filter(c => !c.command.includes(`%ENABLESHORTCUTS%`)) + .map(async (command, index) => + { + const label = command.label; + let cmd = command.command; + + let emulator: string | undefined = undefined; + let rom = validFiles[0]; + + if (cmd.includes('%ESCAPESPECIALS%')) + rom = rom.replace(/[&()^=;,]/g, ''); + + + + const staticVars: Record = { + '%ROM%': escapeWindowsArg(rom), + '%ROMRAW%': validFiles[0], + '%ROMRAWWIN%': escapeWindowsArg(validFiles[0].replaceAll('/', '\\')), + '%ESPATH%': escapeWindowsArg(path.dirname(Bun.main)), + '%ROMPATH%': escapeWindowsArg(gamePath), + '%BASENAME%': escapeWindowsArg(path.basename(validFiles[0], path.extname(validFiles[0]))), + '%FILENAME%': escapeWindowsArg(path.basename(validFiles[0])), + '%ESCAPESPECIALS%': "", + '%HIDEWINDOW%': "" + }; + + cmd = cmd.replace(/\%INJECT\%=(?[\w\%.\/\\]+)/g, (_, injectFile: string) => + { + try + { + const resolvedInjectFile = injectFile.replace(this.varRegex, (a) => + { + return staticVars[a] ?? a; + }); + if (existsSync(resolvedInjectFile)) + { + const rawContents = readFileSync(resolvedInjectFile, { encoding: 'utf-8' }); + return rawContents.split('\n').map(v => v.replace('\r', '')).join(' '); + } + + return ''; + } catch (error) + { + return ''; + } + }); + + const matches = Array.from(cmd.matchAll(this.varRegex)); + const varList = await Promise.all(matches.map(async ([value]) => + { + if (value.startsWith("%EMULATOR_")) + { + const emulatorName = value.substring("%EMULATOR_".length, value.length - 1); + let execs = await this.findExecsByName(emulatorName); + let validExec = execs.find(e => e.exists); + + emulator = emulatorName; + return [ + [value, validExec ? validExec.binPath : undefined] as [string, string | undefined], + [`%EMUSOURCE%`, validExec?.type] as [string, string | undefined], + ['%EMUDIR%', validExec?.rootPath ?? (validExec ? escapeWindowsArg(path.dirname(validExec.binPath)) : undefined)] as [string, string | undefined], + ['%EMUDIRRAW%', validExec?.rootPath ?? (validExec ? path.dirname(validExec.binPath) : undefined)] as [string, string | undefined] + ]; + + } + + const key = value[0].substring(1, value.length - 1); + return [[value, process.env[key]] as [string, string | undefined]]; + })); + + const vars = { ...Object.fromEntries(varList.flatMap(l => l)), ...staticVars }; + let startDir: string | undefined = undefined; + + if ('%STARTDIR%' in vars) + { + delete vars['%STARTDIR%']; + + cmd = cmd.replace(this.assignRegex, (match, p1, p2) => + { + if (p1 === '%STARTDIR%') + { + startDir = this.varRegex.test(p2) ? staticVars[p2] : p2; + } + return ""; + }); + } + + // missing variable + const invalid = Object.entries(vars).find(c => c[1] === undefined); + + const formattedCommand = cmd.replace(this.varRegex, (s) => vars[s] ?? '').trim(); + + return { + id: index, + label: label ?? undefined, + command: formattedCommand, + startDir, + valid: !invalid, emulator, + emulatorSource: vars['%EMUSOURCE%'] as any, + metadata: { + romPath: validFiles[0], + emulatorBin: varList.flatMap(l => l).find(v => v[0].includes('%EMULATOR_'))?.[1], + emulatorDir: vars['%EMUDIRRAW%'] + } + } satisfies CommandEntry; + })); + + return formattedCommands.filter(c => !!c); + } + + async load (ctx: PluginLoadingContextType) + { + ctx.hooks.emulators.findEmulatorSource.tapPromise(desc.name, async ({ sources, emulator }) => + { + sources.push(...await this.findExecsByName(emulator)); + }); + + ctx.hooks.emulators.findEmulatorForSystem.tapPromise(desc.name, async ({ system, emulators }) => + { + emulators.push(...await this.getEmulatorsForSystem(system)); + }); + + ctx.hooks.games.fetchRomFiles.tapPromise(desc.name, async ({ source, id }) => + { + const localGame = await db.query.games.findFirst({ + where: getLocalGameMatch(id, source), + columns: { path_fs: true, main_glob: true }, + with: { platform: { columns: { es_slug: true } } } + }); + + if (!localGame?.path_fs) + { + return; + } + + const downloadPath = config.get('downloadPath'); + const path_fs = path.isAbsolute(localGame.path_fs) ? localGame.path_fs : path.join(downloadPath, localGame.path_fs); + + return this.getRomFilePaths(path_fs, { systemSlug: localGame.platform.es_slug ?? undefined, mainGlob: localGame.main_glob }); + }); + + ctx.hooks.games.buildLaunchCommands.tapPromise(desc.name, async ({ systemSlug, source, id, gamePath, mainGlob }) => + { + if (source === 'emulator') + { + const esEmulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, id.id) }); + const allExecs = await this.findExecs(id.id, esEmulator); + return allExecs.map(exec => ({ + command: exec.binPath, + id: exec.type, + emulator: id.id, + emulatorSource: exec.type, + metadata: { + emulatorBin: exec.binPath, + emulatorDir: exec.rootPath + }, + valid: true + } satisfies CommandEntry)); + } + + const rommPlatform = systemSlug; + let esSystem: string | undefined = undefined; + const systemMapping = await emulatorsDb.query.systemMappings.findFirst({ + where: and(eq(emulatorSchema.systemMappings.sourceSlug, rommPlatform), eq(emulatorSchema.systemMappings.source, 'romm')) + }); + + if (systemMapping) esSystem = systemMapping.system; + + if (!esSystem) + { + const system = await emulatorsDb.query.systems.findFirst({ where: eq(emulatorSchema.systems.name, systemSlug), columns: { name: true } }); + if (system) esSystem = system.name; + } + + if (esSystem && gamePath) + { + try + { + const commands = await this.getValidLaunchCommands({ systemSlug: esSystem, gamePath, mainGlob }); + + if (cores[esSystem]) + { + const gameUrl = `${RPC_URL(host)}/api/romm/rom/${id.source}/${id.id}`; + commands.push({ + id: 'EMULATORJS', + label: "Emulator JS", + command: `core=${cores[esSystem]}&gameUrl=${encodeURIComponent(gameUrl)}`, + valid: true, + emulator: 'EMULATORJS', + metadata: { + romPath: gameUrl + } + }); + } + + return commands; + } catch (error) + { + console.error(error); + if (error instanceof Error) return error; + } + } + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/package.json b/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/package.json new file mode 100644 index 0000000..90815b7 --- /dev/null +++ b/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/package.json @@ -0,0 +1,14 @@ +{ + "name": "com.simeonradivoev.gameflow.es", + "displayName": "ES-DE Launcher", + "version": "0.0.1", + "description": "ES-DE Launch Configurations. Used as fallback", + "main": "./es-de.ts", + "icon": "https://impro.usercontent.one/appid/oneComWsb/domain/es-de.org/media/es-de.org/onewebmedia/ES-DE_logo.png", + "canDisable": false, + "category": "launchers", + "keywords": [ + "integration", + "es-de" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/package.json b/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/package.json new file mode 100644 index 0000000..2c42339 --- /dev/null +++ b/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/package.json @@ -0,0 +1,13 @@ +{ + "name": "com.simeonradivoev.gameflow.rclone", + "displayName": "Rclone Integration", + "version": "0.0.1", + "description": "Rclone integration for syncing saves", + "main": "./rclone.ts", + "icon": "data:image/svg+xml,%3Csvg%20role%3D%22img%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3ERclone%3C%2Ftitle%3E%3Cpath%20d%3D%22M11.842.6258C9.3647.6813%206.9754%201.9906%205.646%204.2933c-.7593%201.3144-1.0647%202.7662-.966%204.1745a7.99%207.99%200%200%201%202.6568-.4541l1.4705-.0013c-.0093-.5594.1245-1.1284.4245-1.6482.8827-1.5284%202.837-2.0522%204.3654-1.1695%201.5284.8824%202.0519%202.8366%201.1695%204.365l-1.4782%202.5647%201.1955%202.0714%202.3914-.0004%201.4775-2.5655c2.0262-3.5088.8239-7.9959-2.6853-10.0217C14.4614.9118%2013.1396.5967%2011.842.6258m-1.5451%208.073-2.9605.0029C3.2844%208.7017%200%2011.9867%200%2016.0383c0%204.052%203.2844%207.3367%207.3364%207.3367%201.5174%200%202.9267-.4609%204.0967-1.2497a8%208%200%200%201-1.72-2.0748l-.7368-1.273c-.4799.288-1.0392.4565-1.6395.4565-1.765%200-3.1958-1.4307-3.1958-3.1958%200-1.7647%201.4307-3.1954%203.1958-3.1954l2.96-.0022%201.1962-2.0708zm9.587.7475a7.99%207.99%200%200%201-.935%202.5278l-.7344%201.2745c.4892.2717.915.6719%201.2153%201.192.8823%201.528.3585%203.4826-1.1699%204.365-1.528.8823-3.4828.3588-4.3651-1.1696l-1.482-2.5628h-2.3915L8.8256%2017.144l1.483%202.5626c2.0262%203.5091%206.513%204.7112%2010.022%202.685%203.5089-2.0257%204.7112-6.5125%202.6853-10.0216-.7588-1.3144-1.863-2.3052-3.132-2.9237%22%20%2F%3E%3C%2Fsvg%3E", + "category": "saves", + "keywords": [ + "integration", + "rclone" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/rclone.ts b/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/rclone.ts new file mode 100644 index 0000000..8ab31a0 --- /dev/null +++ b/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/rclone.ts @@ -0,0 +1,473 @@ +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import { config, db, events } from "@/bun/api/app"; +import path from 'node:path'; +import unzip from 'unzip-stream'; +import { ensureDir } from "fs-extra"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import fs from 'node:fs/promises'; +import { randomUUIDv7 } from "bun"; +import z from "zod"; +import { createInterface } from "node:readline"; +import { getLocalGameMatch } from "@/bun/api/games/services/utils"; +import { getErrorMessage } from "@/bun/utils"; + +const DefaultLocalName = "Default_Local"; + +const SettingsSchema = z.object({ + runWebGui: z.boolean() + .default(false) + .describe("Run the Web GUI that can be accessed at http://localhost:5572") + .meta({ title: "Run Web GUI" }), + globalConfig: z.boolean().default(false).describe("Use the Global Config file if already setup"), + webGuiPassword: z.string().optional().readonly().describe("Randomly Generated. Read Only. Username is gameflow"), + remoteName: z.string().default(DefaultLocalName), + verboseLog: z.boolean() + .default(false) + .describe("Show detailed log of operation for debugging") + .meta({ $comment: JSON.stringify({ category: "debug" }) }), + importSaves: z.boolean().default(true).describe("Import Saves From the Destination. This will override local saves"), + exportSaves: z.boolean().default(true).describe("Export saves to remove. This will sync current saves with remote") +}); + +type SettingsType = z.infer; +const loginTokenUrlRegex = /http:\/\/[\w\d:\-@\[\]\.\/?=]+/gm; + +export default class RcloneIntegration implements PluginType +{ + settingsSchema = SettingsSchema; + rclonePath: string | undefined; + server: Bun.Subprocess | undefined; + password: string; + user = "gameflow"; + loginUrl: string | undefined = undefined; + eventsNames = [{ + id: "open-web-gui", + title: "Open Web GUI", + description: "Open Web GUI", + action: "Open" + }, { + id: "refresh", + title: "Refresh Sources", + action: "Refresh" + }]; + + constructor() + { + this.password = randomUUIDv7(); + } + + async onEvent (id: string) + { + switch (id) + { + case "open-web-gui": + return { openTab: this.loginUrl }; + break; + case "refresh": + await this.refresh(); + return { reload: true }; + break; + } + } + + async setup (ctx: PluginLoadingContextType) + { + ctx.zodRegistry.add(SettingsSchema.shape.runWebGui, { requiresRestart: true }); + ctx.zodRegistry.add(SettingsSchema.shape.globalConfig, { requiresRestart: true }); + + const toolsPath = path.join(config.get('downloadPath'), "tools"); + await ensureDir(toolsPath); + const binaryMap: Record = { + win32: '**/rclone.exe', + linux: 'rclone-*/rclone', + darwin: 'rclone-*/rclone' + }; + const existingRclones = await Array.fromAsync(fs.glob(binaryMap[process.platform], { cwd: toolsPath })); + if (existingRclones[0]) + { + this.rclonePath = path.join(toolsPath, existingRclones[0]); + await this.startServer(ctx); + return; + } + + ctx.setProgress(0.5, "Downloading RClone"); + const platformMap: Record = { + linux: "linux", + win32: "windows", + darwin: "osx" + }; + const archMap: Record = { + x64: "amd64", + arm64: "arm64" + }; + const downloadUrl = `https://downloads.rclone.org/rclone-current-${platformMap[process.platform]}-${archMap[process.arch]}.zip`; + console.log("Starting Download", downloadUrl); + const rcCloseZip = await fetch(downloadUrl); + + await ensureDir(toolsPath); + await pipeline(Readable.fromWeb(rcCloseZip.body as any), unzip.Extract({ path: toolsPath })); + const dests = await Array.fromAsync(fs.glob(binaryMap[process.platform], { cwd: toolsPath })); + if (dests[0]) + { + this.rclonePath = path.join(toolsPath, dests[0]); + await fs.chmod(this.rclonePath, 0o755); + await this.startServer(ctx); + return; + } + } + + async refresh () + { + try + { + const data = await this.request('/config/listremotes', {}); + z.globalRegistry.add(SettingsSchema.shape.remoteName, { + examples: [''].concat(...data.remotes), + description: "The name of the remote to sync with" + }); + } catch (error) + { + events.emit('notification', { message: getErrorMessage(error), type: 'error' }); + z.globalRegistry.add(SettingsSchema.shape.remoteName, { + examples: [''], + description: "The name of the remote to sync with" + }); + } + } + + async startServer (ctx: PluginLoadingContextType) + { + const args: string[] = []; + if (ctx.config.get('runWebGui')) + { + args.push("--rc-web-gui"); + args.push("--rc-web-gui-no-open-browser"); + } + if (ctx.config.get('')) + { + args.push('-vv'); + } + let env: Record | undefined = undefined; + if (!ctx.config.get('globalConfig')) + { + env = { RCLONE_CONFIG: path.join(config.get('downloadPath'), 'tools', 'config', 'rclone', 'rclone.conf') }; + } + ctx.config.set('webGuiPassword', this.password); + this.server = Bun.spawn([this.rclonePath!, "rcd", '--use-json-log', `--rc-user=${this.user}`, ...args, `--rc-pass=${this.password}`, "--rc-addr", "localhost:5572"], { + stdout: "pipe", + stderr: "pipe", + env + }); + const rl = createInterface({ input: Readable.fromWeb(this.server.stderr as any) }); + rl.on('line', e => + { + try + { + const data = JSON.parse(e); + + if (data.level === 'error') + { + console.error(data.msg); + } else if (data.level === 'critical') + { + console.error(data.msg); + } + + else + { + console.log(e); + if (loginTokenUrlRegex.test(data.msg)) + { + this.loginUrl = (data.msg as string).match(loginTokenUrlRegex)?.find(e => e); + } + } + } catch (error) + { + console.log(e); + } + + }); + + await new Promise((resolve, reject) => + { + const handleResolve = (line: string) => + { + try + { + const data = JSON.parse(line); + if (!loginTokenUrlRegex.test(data.msg)) return; + rl.off('line', handleResolve); + resolve(data); + } catch (error) + { + + } + }; + rl.on('line', handleResolve); + setTimeout(() => { reject("Timeout"); }, 5000); + }); + + await this.refresh(); + } + + async request (path: string, body: any) + { + const response = await fetch(`http://localhost:5572${path}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Basic ${Buffer.from(`${this.user}:${this.password}`).toString('base64')}` + }, + body: JSON.stringify(body) + }); + + const data = await response.json(); + if (response.ok) + { + return data; + } else + { + throw new Error(response.statusText, { cause: data }); + } + } + + async cleanup () + { + await new Promise((resolve) => + { + this.request('/core/quit', {}).catch(e => + { + this.server?.kill("SIGKILL"); + this.server = undefined; + }); + + setTimeout(() => + { + this.request('/core/quit', { exitCode: 9 }).then(e => + { + resolve(false); + this.server = undefined; + }).catch(e => + { + resolve(false); + this.server?.kill("SIGKILL"); + this.server = undefined; + }); + + + }, 5000); + + this.server?.exited.then(() => resolve(true)); + }); + + } + + async load (ctx: PluginLoadingContextType) + { + await this.setup(ctx); + + ctx.hooks.games.prePlay.tapPromise({ + name: desc.name, + stage: 10, + }, async ({ source, id, setProgress, saveFolderSlots, command }) => + { + if (!this.rclonePath || !saveFolderSlots || !ctx.config.get('importSaves')) return; + + const destination = source === 'store' ? [source, id] : command.emulator ? [command.emulator] : undefined; + if (!destination) return; + + const remoteName = ctx.config.get('remoteName'); + + for await (const [slot, { cwd }] of Object.entries(saveFolderSlots)) + { + let supportsMetadata = true; + let src: string; + + if (remoteName && remoteName !== DefaultLocalName) + { + src = `${remoteName}:gameflow/saves/${destination.join('/')}/${slot}`; + + const exists = await this.request('/operations/stat', { + fs: `${remoteName}:`, + remote: `gameflow/saves/${destination.join('/')}/${slot}` + }).catch(e => undefined); + if (!exists || !exists.item) return; + const remote = await this.request('/operations/fsinfo', { + fs: `${remoteName}:` + }); + supportsMetadata = !remote.ReadMetadata; + if (supportsMetadata) + { + console.warn("Remote", remoteName, "does not support metadata"); + } + } else + { + src = path.join(config.get('downloadPath'), 'saves', ...destination, slot); + if (!await fs.exists(path.join(config.get('downloadPath'), 'saves', ...destination, slot))) return; + } + + const job = await this.request('/sync/copy', { + srcFs: src, + dstFs: cwd, + createEmptySrcDirs: true, + _async: true, + _config: { + CheckFirst: true, + Metadata: true, + NoCheckDest: supportsMetadata + } + }).catch(e => + { + events.emit('notification', { message: `RClone: ${e.cause?.error ?? e.message ?? e}`, type: 'error' }); + return undefined; + });; + + await new Promise(async (resolve, reject) => + { + setProgress(0, "RClone: Syncing Saves"); + + const checkInterval = setInterval(async () => + { + const stat = await this.request('/job/status', { jobid: job.jobid }); + if (stat.finished) + { + clearInterval(checkInterval); + console.log(stat.output); + resolve(true); + + } else if (stat.error) + { + reject(stat.error); + } else + { + setProgress(stat.progress, "RClone: Syncing Saves"); + } + }, 500); + }); + } + + }); + + ctx.hooks.games.postPlay.tapPromise({ name: desc.name, stage: 10 }, async ({ source, id, validChangedSaveFiles, command }) => + { + if (!this.rclonePath || !ctx.config.get('exportSaves')) return; + const local = await db.query.games.findFirst({ where: getLocalGameMatch(id, source) }); + console.log("Save Files", Object.values(validChangedSaveFiles).flatMap(c => Array.isArray(c.subPath) ? c.subPath : [c.subPath]).join(",")); + + const destination = source === 'store' ? [source, id] : command.emulator ? [command.emulator] : undefined; + if (!destination) return; + + const remoteName = ctx.config.get('remoteName'); + + await Promise.all(Object.entries(validChangedSaveFiles).map(async ([slot, change]) => + { + let suportsMetadata = false; + let dest: string; + if (remoteName && remoteName !== DefaultLocalName) + { + dest = `${remoteName}:gameflow/saves/${destination.join('/')}/${slot}`; + const remote = await this.request('/operations/fsinfo', { + fs: `${remoteName}:` + }); + suportsMetadata = !remote.ReadMetadata; + if (suportsMetadata) + { + console.warn("Remote", remoteName, "does not support metadata"); + } + } else + { + dest = path.join(config.get('downloadPath'), 'saves', ...destination, slot); + } + + const filter = { + IncludeRule: Array.isArray(change.subPath) ? + change.subPath.map(s => + { + if (change.isGlob) return s; + else s.replaceAll('\\', '/'); + }) : + [change.isGlob ? change.subPath : change.subPath.replaceAll('\\', '/')] + }; + + let jobid: number | undefined = undefined; + + if (change.fixedSize) + { + await this.request('/sync/copy', { + srcFs: change.cwd, + dstFs: dest, + createEmptySrcDirs: true, + _async: true, + _config: { + NoCheckDest: true + }, + _filter: filter + }) + .then(job => jobid = job.jobid) + .catch(e => + { + events.emit('notification', { message: `RClone: ${e.cause?.error ?? e.message ?? e}`, type: 'error' }); + return undefined; + }); + } else + { + await this.request('/sync/sync', { + srcFs: change.cwd, + dstFs: dest, + createEmptySrcDirs: true, + _async: true, + _config: { + CheckSum: true, + CheckFirst: true, + Metadata: true, + MetadataSet: { + igdb_id: local?.igdb_id ? String(local?.igdb_id) : undefined, + ra_id: local?.ra_id ? String(local?.ra_id) : undefined + } + }, + _filter: filter + }) + .then(job => jobid = job.jobid) + .catch(e => + { + events.emit('notification', { message: `RClone: ${e.cause?.error ?? e.message ?? e}`, type: 'error' }); + return undefined; + }); + } + + if (!jobid) return; + await new Promise(async (resolve, reject) => + { + const checkInterval = setInterval(async () => + { + const stat = await this.request('/job/status', { jobid }); + if (stat.finished) + { + clearInterval(checkInterval); + console.log(stat.output); + resolve(true); + + } else if (stat.error) + { + reject(stat.error); + } else + { + + } + }, 500); + }); + + const stats = await this.request('/core/stats', { + group: `job/${jobid}` + }); + + if (stats.transfers > 0) + { + events.emit('notification', { message: "RClone: Save Synced", type: 'success', icon: 'save' }); + } + })); + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts new file mode 100644 index 0000000..c2be6d3 --- /dev/null +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts @@ -0,0 +1,125 @@ +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import secrets from "@/bun/api/secrets"; +import PQueue from 'p-queue'; +import * as igdb from '@phalcode/ts-igdb-client'; +import { checkLoginAndRefreshTwitch } from "@/bun/api/auth"; +import { GameLookup } from "@simeonradivoev/gameflow-sdk/shared"; + +export default class IgdbIntegration implements PluginType +{ + queue: PQueue; + + constructor() + { + this.queue = new PQueue({ concurrency: 8, interval: 1000, intervalCap: 4, strict: true }); + } + + async apiCall (subPath: string, query: string) + { + const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); + const headers = new Headers({ + "Client-ID": process.env.TWITCH_CLIENT_ID ?? '', + Authorization: `Bearer ${access_token}`, + Accept: "application/json" + }); + const response = await this.queue.add(() => fetch(`https://api.igdb.com/v4${subPath}`, { + headers: headers, + method: "POST", + body: query + })); + if (response.ok) + { + return response.json() as T; + } + } + + async cleanup () + { + this.queue.clear(); + } + + async load (ctx: PluginLoadingContextType) + { + await checkLoginAndRefreshTwitch(); + + ctx.hooks.games.gameLookup.tapPromise(desc.name, async (matches, { source, id, search }) => + { + if (!process.env.TWITCH_CLIENT_ID) return matches; + const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); + if (!access_token) + { + return matches; + } + + if ((source === 'igdb' && id) || search) + { + const client = igdb.igdb(process.env.TWITCH_CLIENT_ID, access_token); + + const { data: games } = await this.queue.add(() => client.request('games') + .pipe(...(search ? [igdb.search(search)] : []), + igdb.fields(['id', 'name', 'summary', 'screenshots.image_id', 'slug', 'first_release_date', 'rating', 'genres.name', 'involved_companies.company.name', 'keywords.name', 'game_modes.name', 'cover.image_id', 'age_ratings.rating_category.rating', 'platforms.name', 'platforms.abbreviation', 'platforms.slug']), + ...(source === 'igdb' && id ? [igdb.where('id', '=', Number(id))] : []), + igdb.limit(10)).execute()); + + matches.set(desc.name, games.filter(g => !!g.name) + .map(g => + { + const lookup: GameLookup = { + source: 'igdb', + id: String(g.id), + coverUrl: g.cover ? `https://images.igdb.com/igdb/image/upload/t_720p/${g.cover.image_id}.webp` : undefined, + screenshotUrls: g.screenshots?.map(s => `https://images.igdb.com/igdb/image/upload/t_720p/${s.image_id}.webp`) ?? [], + name: g.name!, + summary: g.summary, + genres: g.genres?.map(g => g.name!) ?? [], + companies: g.involved_companies?.filter(c => c.company?.name).map(c => c.company?.name!) ?? [], + game_modes: g.game_modes?.map(m => m.name!) ?? [], + age_ratings: g.age_ratings?.map(r => r.rating_category?.rating!) ?? [], + player_count: undefined, + // UNIX date, needs to be converted + first_release_date: g.first_release_date ? g.first_release_date * 1000 : undefined, + average_rating: g.rating ?? undefined, + keywords: g.keywords?.map(k => k.name!) ?? [], + igdb_id: g.id ?? undefined, + platforms: g.platforms?.map(p => ({ id: p.id!, name: p.abbreviation, displayName: p.name!, slug: p.slug! })) ?? [], + slug: g.slug + }; + + return lookup; + })); + + return matches; + } + + return matches.set(desc.name, []); + }); + + ctx.hooks.games.platformLookup.tapPromise(desc.name, async ({ source, id, slug }) => + { + let query: string | undefined = undefined; + if (source && id) + { + if (source !== 'igdb') return; + query = `fields name, slug, platform_logo.image_id, platform_logo.url, platform_family.name; where id = ${id};`; + + } + else if (slug) + { + query = `fields name, slug, platform_logo.image_id, platform_logo.url, platform_family.name; where slug = "${slug}";`; + } + + if (query) + { + const data = await this.apiCall<[any]>('/platforms', query); + if (!data || data.length <= 0) return; + return { + slug: data[0].slug, + url_logo: `https://images.igdb.com/igdb/image/upload/t_logo_med/${data[0].platform_logo.image_id}.png`, + name: data[0].name, + family_name: data[0].platform_family?.name + }; + } + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/package.json b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/package.json new file mode 100644 index 0000000..55939bb --- /dev/null +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/package.json @@ -0,0 +1,13 @@ +{ + "name": "com.simeonradivoev.gameflow.igdb", + "displayName": "IGDB Integration", + "version": "0.0.1", + "description": "IGDB Metadata Integration", + "main": "./igdb.ts", + "icon": "data:image/svg+xml,%3Csvg%20role%3D%22img%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EIGDB%3C%2Ftitle%3E%3Cpath%20d%3D%22M24%206.228c-8%20.002-16%200-24%200v11.543a88.875%2088.875%200%200%201%202.271-.333%2074.051%2074.051%200%200%201%2017.038-.28c1.57.153%203.134.363%204.69.614V6.228zm-.706.707v10.013a74.747%2074.747%200%200%200-22.588%200V6.934h22.588ZM7.729%208.84a2.624%202.624%200%200%200-1.857.72%202.55%202.55%200%200%200-.73%201.33c-.098.5-.063%201.03.112%201.51.177.488.515.917.954%201.196.547.354%201.224.472%201.865.401a3.242%203.242%200%200%200%201.786-.777c-.003-.724.002-1.449-.002-2.173-.725.004-1.45-.002-2.174.003.003.317%200%20.634.001.951h1.105c.002.236%200%20.473.002.71-.268.196-.603.286-.932.298-.32.02-.65-.05-.922-.225a1.464%201.464%200%200%201-.59-.744c-.18-.499-.134-1.085.163-1.53.23-.355.619-.61%201.043-.647a1.8%201.8%200%200%201%201.012.206c.152.082.286.192.424.295.228-.281.461-.559.692-.838a3.033%203.033%200%200%200-.595-.403c-.418-.212-.892-.285-1.357-.283Zm11.66.086c-.093%200-.187.002-.28%200-.68.002-1.359-.004-2.038.003.003%201.666%200%203.332.002%204.998h2.497c.239-.002.478-.034.709-.097.276-.076.546-.208.742-.422.194-.208.297-.492.304-.776.016-.278-.032-.572-.195-.804-.175-.252-.453-.408-.734-.514.211-.122.407-.285.521-.505.134-.246.149-.535.117-.807a1.156%201.156%200%200%200-.436-.73c-.264-.207-.599-.304-.93-.334a2.757%202.757%200%200%200-.279-.012Zm-16.715%200v5.002h1.102V8.927c-.368-.002-.735%200-1.102%200zm8.524%200v5.002h2.016a2.87%202.87%200%200%200%201.07-.211%202.445%202.445%200%200%200%201.174-.993c.34-.555.429-1.244.292-1.876a2.367%202.367%200%200%200-.828-1.338c-.478-.387-1.096-.577-1.707-.584h-2.017zm6.949.967c.392.002.784-.001%201.176.002.183.011.38.054.51.19.11.112.136.28.112.43a.436.436%200%200%201-.22.316%201.082%201.082%200%200%201-.483.116c-.365.002-.73-.001-1.094.001-.002-.351%200-.703-.001-1.054zm-5.031.026c.28%200%20.567.053.815.19.274.149.491.396.607.685.113.272.138.574.107.865a1.456%201.456%200%200%201-.335.786%201.425%201.425%200%200%201-.865.466c-.168.031-.34.022-.51.023h-.632V9.92h.813zm5.03%201.948h1.36c.174.006.354.035.505.127.11.066.191.18.212.308.025.15.004.32-.099.44-.102.12-.258.176-.409.2-.172.032-.348.02-.522.022-.35-.001-.698.002-1.047-.001v-1.096z%22%20%2F%3E%3C%2Fsvg%3E", + "category": "sources", + "keywords": [ + "integration", + "igdb" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/package.json b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/package.json new file mode 100644 index 0000000..52c2376 --- /dev/null +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/package.json @@ -0,0 +1,13 @@ +{ + "name": "com.simeonradivoev.gameflow.romm", + "displayName": "ROMM Integration", + "version": "0.0.1", + "description": "ROMM Server Integration", + "main": "./romm.ts", + "icon": "https://romm.app/_ipx/q_80/images/blocks/logos/romm.svg", + "category": "sources", + "keywords": [ + "integration", + "romm" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts new file mode 100644 index 0000000..2be6e68 --- /dev/null +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts @@ -0,0 +1,639 @@ + + +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomByMetadataProviderApiRomsByMetadataProviderGet, getRomContentApiRomsIdContentFileNameGet, getRomFiltersApiRomsFiltersGet, getRomsApiRomsGet, getSavesSummaryApiSavesSummaryGet, PlatformSchema, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm"; +import { config, events } from "@/bun/api/app"; +import path from 'node:path'; +import fs from 'node:fs/promises'; +import { hashFile, isArchive, isSteamDeckGameMode } from "@/bun/utils"; +import { CACHE_KEYS, getOrCached } from "@/bun/api/cache"; +import secrets from "@/bun/api/secrets"; +import { getAuthToken } from "@/clients/romm/core/auth.gen"; +import { client } from "@/clients/romm/client.gen"; +import { validateGameSource } from "@/bun/api/games/services/statusService"; +import z from "zod"; +import { checkLoginAndRefreshRomm } from "@/bun/api/auth"; +import { DownloadFileEntry, DownloadInfo, FrontEndCollection, FrontEndGameType, FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement, FrontEndGameTypeWithIds, FrontEndPlatformType } from "@simeonradivoev/gameflow-sdk/shared"; +import Conf from "conf"; + +const SettingsSchema = z.object({ + savesSync: z.boolean().default(false).describe("Experimental save sync support"), + clientApiToken: z.string().optional().describe("Generate a long lived token from the ROMM server") +}); + +type SettingsType = z.infer; + +export default class RommIntegration implements PluginType +{ + settingsSchema = SettingsSchema; + isSteamDeck = false; + orderByMap: Record = { + added: "created_at", + activity: "created_at", + name: "name", + release: "metadatum.first_release_date" + }; + + async checkRemote () + { + if (!config.has('rommAddress')) return false; + return true; + } + + async getAccessToken (config: Conf) + { + if (process.env.ROMM_CLIENT_TOKEN) return process.env.ROMM_CLIENT_TOKEN; + const client_token = config.get('clientApiToken'); + if (client_token) return client_token; + return (await secrets.get({ service: 'gameflow', name: 'romm_access_token' })) ?? undefined; + } + + async updateClient (pluginConfig: Conf) + { + client.setConfig({ + baseUrl: config.get('rommAddress'), + auth: (auth) => + { + if (auth.scheme === 'bearer') + { + return this.getAccessToken(pluginConfig); + } + } + }); + } + + async getAuthToken (config: Conf) + { + return getAuthToken({ + scheme: 'bearer', + type: "http" + }, async (a) => this.getAccessToken(config)); + } + + async getAllRommPlatforms () + { + return getOrCached(CACHE_KEYS.ROM_PLATFORMS, () => getPlatformsApiPlatformsGet({ throwOnError: true }), { expireMs: 60 * 60 * 1000 }).then(d => d.data); + } + + convertRomToFrontend (rom: SimpleRomSchema) + { + const game: FrontEndGameType = { + id: { id: String(rom.id), source: 'romm' }, + path_covers: [`/api/romm/image/romm${this.isSteamDeck ? rom.path_cover_small : rom.path_cover_large}`], + last_played: rom.rom_user.last_played !== null ? new Date(rom.rom_user.last_played) : null, + updated_at: new Date(rom.created_at), + metadata: { + first_release_date: rom.metadatum.first_release_date !== null ? new Date(rom.metadatum.first_release_date) : null, + }, + slug: rom.slug, + platform_id: rom.platform_id, + platform_display_name: rom.platform_display_name, + name: rom.name, + path_fs: null, + path_platform_cover: `/api/romm/image/romm/assets/platforms/${rom.platform_slug}.svg`, + source: null, + source_id: null, + paths_screenshots: rom.merged_screenshots.map(s => `/api/romm/image/romm/${s}`), + platform_slug: rom.platform_slug + }; + + return game; + } + + async convertRomToFrontendDetailed (rom: DetailedRomSchema) + { + const detailed: FrontEndGameTypeDetailed = { + ...this.convertRomToFrontend(rom), + summary: rom.summary, + fs_size_bytes: rom.fs_size_bytes, + local: false, + missing: rom.missing_from_fs, + igdb_id: rom.igdb_id, + ra_id: rom.ra_id, + metadata: { + age_ratings: rom.metadatum.age_ratings, + genres: rom.metadatum.genres, + companies: rom.metadatum.companies, + game_modes: rom.metadatum.game_modes, + player_count: rom.metadatum.player_count, + average_rating: rom.metadatum.average_rating, + first_release_date: rom.metadatum.first_release_date ? new Date(rom.metadatum.first_release_date) : null + } + }; + + const userData = await getCurrentUserApiUsersMeGet(); + const gameAchievements = userData.data?.ra_progression?.results?.find(p => p.rom_ra_id == rom.ra_id); + + if (rom.merged_ra_metadata?.achievements) + { + const earnedMap = new Map(gameAchievements?.earned_achievements.map(a => [a.id, { date: new Date(a.date), date_hardcore: a.date_hardcore ? new Date(a.date_hardcore) : undefined }])); + detailed.achievements = { + unlocked: gameAchievements?.num_awarded ?? 0, + entires: rom.merged_ra_metadata.achievements.map(a => + { + const earned = a.badge_id ? earnedMap.get(a.badge_id) : undefined; + const ach: FrontEndGameTypeDetailedAchievement = { + id: a.badge_id ?? String(a.ra_id) ?? 'unknown', + title: a.title ?? "Unknown", + badge_url: (earned ? a.badge_url : a.badge_url_lock) ?? undefined, + date: earned?.date, + date_hardcode: earned?.date_hardcode, + description: a.description ?? undefined, + display_order: a.display_order ?? 0, + type: a.type ?? undefined + }; + + return ach; + }).sort((a, b) => a.display_order - b.display_order), + total: rom.merged_ra_metadata.achievements.length + }; + } + return detailed; + } + + async load (ctx: PluginLoadingContextType) + { + this.isSteamDeck = isSteamDeckGameMode(); + ctx.setProgress(0, "Logging Into Romm"); + await this.updateClient(ctx.config); + await checkLoginAndRefreshRomm(); + await this.updateClient(ctx.config); + + ctx.hooks.games.fetchGames.tapPromise(desc.name, async ({ query, games }) => + { + if (!await this.checkRemote()) return; + if (((!query.platform_source || query.platform_source === 'romm') || !!query.collection_id) && (!query.source || query.source === 'romm')) + { + const rommGames = await getRomsApiRomsGet({ + query: { + platform_ids: query.platform_id ? [query.platform_id] : undefined, + collection_id: query.collection_id, + limit: query.limit, + offset: query.offset, + order_by: this.orderByMap[query.orderBy ?? ''], + with_filter_values: false, + genres: query.genres, + genres_logic: "all", + age_ratings: query.age_ratings, + search_term: query.search, + }, throwOnError: true + }); + + games.push(...rommGames.data.items.map(g => + { + const game: FrontEndGameTypeWithIds = { + ...this.convertRomToFrontend(g), + igdb_id: g.igdb_id, + ra_id: g.ra_id + }; + return game; + })); + } + }); + + ctx.hooks.games.fetchFilters.tapPromise(desc.name, async ({ filters, source }) => + { + if (!await this.checkRemote()) return; + if (source && source !== 'romm') return; + + const rommFilters = await getRomFiltersApiRomsFiltersGet({ throwOnError: true }); + rommFilters.data.age_ratings.forEach(r => filters.age_ratings.add(r)); + rommFilters.data.companies.forEach(r => filters.companies.add(r)); + rommFilters.data.languages.forEach(r => filters.languages.add(r)); + rommFilters.data.player_counts.forEach(r => filters.player_counts.add(r)); + rommFilters.data.genres.forEach(r => filters.genres.add(r)); + }); + + ctx.hooks.auth.loginComplete.tapPromise(desc.name, async ({ service }) => + { + if (!await this.checkRemote()) return; + if (service !== 'romm') return; + await this.updateClient(ctx.config); + }); + + ctx.hooks.games.fetchGame.tapPromise(desc.name, async ({ source, id }) => + { + if (!await this.checkRemote()) return; + if (source !== 'romm') return; + + const rom = await getRomApiRomsIdGet({ path: { id: Number(id) } }); + if (rom.data) + { + const romGame = await this.convertRomToFrontendDetailed(rom.data); + return romGame; + } + + return undefined; + }); + + ctx.hooks.games.fetchDownloads.tapPromise(desc.name, async ({ source, id }) => + { + if (!await this.checkRemote()) return; + if (source !== 'romm') return; + + const rom = (await getRomApiRomsIdGet({ path: { id: Number(id) }, throwOnError: true })).data; + const rommPlatform = (await getPlatformApiPlatformsIdGet({ path: { id: rom.platform_id }, throwOnError: true })).data; + const rommAddress = config.get('rommAddress'); + if (!rommAddress) throw new Error("Romm Address Not Defined"); + + const files = await Promise.all(rom.files.map(async f => + { + getRomContentApiRomsIdContentFileNameGet; + const file: DownloadFileEntry = { + url: new URL(`${config.get('rommAddress')}/api/roms/${f.id}/files/content/${f.file_name}`), + file_name: f.file_name, + file_path: f.file_path, + size: f.file_size_bytes, + sha1: f.sha1_hash ?? undefined + }; + return file; + })); + + let extract_path: string | undefined = undefined; + let path_fs = path.join(rom.fs_path, rom.fs_name); + if (files.length === 1) + { + if (isArchive(files[0].file_name)) + { + extract_path = '.'; + path_fs = path.join(rom.fs_path, rom.slug ?? rom.fs_name_no_ext); + } + } + + const info: DownloadInfo = { + platform: { + source: 'romm', + id: String(rommPlatform.id), + slug: rommPlatform.slug, + name: rommPlatform.name, + family_name: rommPlatform.family_name ?? undefined + }, + coverUrl: `${rommAddress}${rom.path_cover_large}`, + screenshotUrls: rom.merged_screenshots.map(s => `${config.get('rommAddress')}${s}`), + last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : undefined, + igdb_id: rom.igdb_id ?? undefined, + ra_id: rom.ra_id ?? undefined, + summary: rom.summary ?? undefined, + name: rom.name ?? "Unknown", + path_fs, + source_id: String(rom.id), + slug: rom.slug ?? undefined, + system_slug: rommPlatform.slug, + metadata: rom.metadatum, + files, + auth: await this.getAuthToken(ctx.config), + extract_path, + id: "romm" + }; + + return [info]; + + }); + + ctx.hooks.emulators.fetchBiosDownload.tapPromise(desc.name, async ({ systems, biosFolder }) => + { + if (!await this.checkRemote()) return; + const files: DownloadFileEntry[] = []; + const allRommPlatforms = await this.getAllRommPlatforms(); + + const rommPlatforms = systems.filter(s => s.romm_slug).map(s => allRommPlatforms.find(p => p.slug == s.romm_slug)).filter(r => !!r); + + for (const rommPlatform of rommPlatforms) + { + const firmwares = await getPlatformFirmwareApiFirmwareGet({ query: { platform_id: rommPlatform.id } }).then(d => d.data); + if (firmwares) + { + for (const firmware of firmwares) + { + const firmwarePath = path.join(biosFolder, firmware.file_name); + const exists = await fs.exists(firmwarePath); + + if (exists && await hashFile(firmwarePath, 'sha1')) + { + return; + } + + files.push({ file_name: firmware.file_name, file_path: '', url: new URL(`http://romm.simeonradivoev.com/api/firmware/${firmware.id}/content/${encodeURIComponent(firmware.file_name)}`) }); + } + } + } + + if (files.length > 0) return { files, auth: await this.getAuthToken(ctx.config) }; + }); + + ctx.hooks.games.fetchRecommendedGamesForGame.tapPromise(desc.name, async ({ game, games }) => + { + if (!await this.checkRemote()) return; + const rommPlatforms = await this.getAllRommPlatforms(); + if (rommPlatforms) + { + const rommPlatform = rommPlatforms.find(p => p.slug === game.platform_slug); + if (rommPlatform) + { + const rommGames = await getRomsApiRomsGet({ query: { genres: game.metadata.genres, genres_logic: 'any' } }); + if (rommGames.data) + { + games.push(...rommGames.data.items.map(g => ({ ...this.convertRomToFrontend(g) }))); + } + } + } + }); + + ctx.hooks.games.fetchRecommendedGamesForEmulator.tapPromise(desc.name, async ({ emulator, games, systems }) => + { + if (!await this.checkRemote()) return; + const rommPlatforms = await this.getAllRommPlatforms(); + const systemsRommSlugSet = new Set(systems.filter(s => s.romm_slug).map(s => s.romm_slug!)); + if (rommPlatforms) + { + const platformIds = rommPlatforms.filter(p => systemsRommSlugSet.has(p.slug)).map(s => s.id); + if (platformIds.length > 0) + { + const rommGames = await getRomsApiRomsGet({ + query: { + platform_ids: platformIds + } + }); + + let gamesPerSystem = Math.round(3 / systemsRommSlugSet.size); + + for (const slug of systemsRommSlugSet) + { + const systemRommGames = rommGames.data?.items.filter(g => slug === g.platform_slug).map(g => + { + return this.convertRomToFrontend(g); + }).slice(0, gamesPerSystem) ?? []; + games.push(...systemRommGames); + } + } + } + }); + + ctx.hooks.games.fetchPlatform.tapPromise(desc.name, async ({ source, id }) => + { + if (!await this.checkRemote()) return; + if (source !== 'romm') return; + const { data: rommPlatform } = await getPlatformApiPlatformsIdGet({ path: { id: Number(id) } }); + if (rommPlatform) + { + const platform: FrontEndPlatformType = { + slug: rommPlatform.slug, + name: rommPlatform.display_name, + family_name: rommPlatform.family_name, + path_cover: `/api/romm/image/romm/assets/platforms/${rommPlatform.slug}.svg`, + game_count: rommPlatform.rom_count, + updated_at: new Date(rommPlatform.updated_at), + id: { source: 'romm', id: String(rommPlatform.id) }, + paths_screenshots: [], + hasLocal: false + }; + + return platform; + } + }); + + ctx.hooks.games.fetchPlatforms.tapPromise(desc.name, async ({ platforms }) => + { + if (!await this.checkRemote()) return; + const rommPlatforms = await this.getAllRommPlatforms().catch(e => + { + console.error(e); + return undefined; + }); + + if (rommPlatforms) + { + const frontEndPlatforms = await Promise.all(rommPlatforms.map(async p => + { + const screenshots: string[] = []; + const rommGames = await getRomsApiRomsGet({ query: { platform_ids: [p.id], limit: 3 } }).then(d => d.data); + if (rommGames) + { + const rommScreenshots = rommGames.items.find(i => i.merged_screenshots.length > 0)?.merged_screenshots.map(s => `/api/romm/image/romm/${s}`); + if (rommScreenshots) + screenshots.push(...rommScreenshots); + } + + const platform: FrontEndPlatformType = { + slug: p.slug, + name: p.display_name, + family_name: p.family_name, + path_cover: `/api/romm/image/romm/assets/platforms/${p.slug}.svg`, + game_count: p.rom_count, + updated_at: new Date(p.updated_at), + id: { source: 'romm', id: String(p.id) }, + hasLocal: false, + paths_screenshots: screenshots + }; + + return platform; + })); + + + platforms.push(...frontEndPlatforms); + } + }); + + ctx.hooks.games.prePlay.tapPromise(desc.name, async ({ source, id, saveFolderSlots, setProgress }) => + { + if (!await this.checkRemote()) return; + if (source !== 'romm' || !ctx.config.get('savesSync')) return; + if (!saveFolderSlots) return; + + for await (const [slot, { cwd }] of Object.entries(saveFolderSlots)) + { + setProgress(0, "saves"); + + const saveFiles = await getSavesSummaryApiSavesSummaryGet({ query: { rom_id: Number(id) } }); + if (saveFiles.error) + { + console.error(saveFiles.error); + } else + { + const rommSlot = saveFiles.data.slots.find(s => s.slot === 'gameflow' && s.latest.file_name_no_tags === slot); + if (rommSlot) + { + const auth = await this.getAuthToken(ctx.config); + const headers: Record = {}; + if (auth) + headers['Authorization'] = auth; + + const saveResponse = await fetch(`${config.get('rommAddress')}${rommSlot.latest.download_path}`, { headers }); + if (!saveResponse.ok) + { + console.error("Error downloading save", saveResponse.statusText); + return; + } + + const saveArchive = new Bun.Archive(await saveResponse.blob()); + setProgress(50, "saves"); + const count = await saveArchive.extract(cwd); + setProgress(100, "saves"); + console.log("Loaded", count, "save files"); + } + } + + setProgress(100, "saves"); + await Bun.sleep(1000); + } + }); + + // Should run after emulators decide on saves + ctx.hooks.games.postPlay.tapPromise({ name: desc.name, stage: 10 }, async ({ source, id, validChangedSaveFiles, command }) => + { + if (!await this.checkRemote()) return; + if (source !== 'romm' || !ctx.config.get('savesSync')) return; + + const sourceValidation = await validateGameSource(source, id); + if (!sourceValidation.valid) + { + console.warn("Invalid Source", sourceValidation.reason, "Skipping updates"); + return; + } + + /*const finalSavePaths = validChangedSaveFiles.filter(f => !f.shared && !f.isGlob).flatMap(s => Array.isArray(s.subPath) ? s.subPath.map(p => ({ cwd: s.cwd, subPath: p })) : [{ cwd: s.cwd, subPath: s.subPath }]); + + const saveFiles = await getSavesSummaryApiSavesSummaryGet({ query: { rom_id: Number(id) } }); + if (saveFiles.error) + { + console.error(saveFiles.error); + } else if (saveFolderPath) + { + for (let i = 0; i < saveFiles.data.slots.length; i++) + { + const slot = saveFiles.data.slots[i]; + const savePath = path.join(saveFolderPath, slot.slot ?? '', `${slot.latest.file_name_no_tags}.${slot.latest.file_extension}`); + if (await fs.exists(savePath)) + { + const stat = await fs.stat(savePath); + if (stat.mtimeMs > new Date(slot.latest.updated_at).getTime()) + { + const subPath = path.join(slot.slot ?? '', `${slot.latest.file_name_no_tags}.${slot.latest.file_extension}`); + if (!finalSavePaths.some(f => f.subPath === subPath)) + { + // Add newer files to the list, maybe they were changed offscreen. + finalSavePaths.push({ subPath, cwd: saveFolderPath }); + } + } + } + } + }*/ + + const finalSavePaths = Object.entries(validChangedSaveFiles).filter(([slot, change]) => !change.isGlob && !change.shared); + + if (finalSavePaths.length > 0) + { + console.log("Files Changed:", finalSavePaths.map(([slot, change]) => Array.isArray(change.subPath) ? change.subPath.join(',') : change.subPath)?.join(", ")); + + await Promise.all(finalSavePaths.map(async ([slot, change]) => + { + const savesArray = Array.isArray(change.subPath) ? change.subPath : [change.subPath]; + + // TODO: handle directories + const archive = new Bun.Archive(Object.fromEntries(savesArray.map(s => [s, Bun.file(path.join(change.cwd, s))]))); + const data: FormData = new FormData(); + data.append('saveFile', await archive.blob(), slot); + + const url = new URL(`${config.get('rommAddress')}/api/saves`); + url.searchParams.set('rom_id', id); + url.searchParams.set('slot', slot); + url.searchParams.set('autocleanup', "true"); + url.searchParams.set('autocleanup_limit', "2"); + if (command.emulator) + url.searchParams.set('emulator', command.emulator); + url.searchParams.set('overwrite', "true"); + + const auth = await this.getAuthToken(ctx.config); + const headers: Record = {}; + if (auth) + headers['Authorization'] = auth; + + const response = await fetch(url, { + body: data, + method: "POST", + headers + }); + if (!response.ok) console.error(response.statusText); + })); + + events.emit('notification', { message: "Saves Uploaded", icon: 'upload', type: "success" }); + } + + const resp = await updateRomUserApiRomsIdPropsPut({ path: { id: Number(id) }, body: { update_last_played: true } }); + if (resp.error) console.error(resp.error); + events.emit('notification', { message: "Updated Played", type: "success", icon: "clock" }); + }); + + ctx.hooks.games.fetchCollections.tapPromise(desc.name, async ({ collections }) => + { + if (!await this.checkRemote()) return; + const rommCollections = await getCollectionsApiCollectionsGet(); + if (rommCollections.response.ok && rommCollections.data) + { + collections.push(...rommCollections.data.map(c => + { + const collection: FrontEndCollection = { + id: { source: 'romm', id: String(c.id) }, + name: c.name, + description: c.description, + game_count: c.rom_count, + path_platform_cover: `/api/romm/image/romm${this.isSteamDeck ? c.path_covers_small ?? c.path_covers_small[0] : c.path_cover_large ?? c.path_covers_large[0]}` + }; + + return collection; + })); + } + }); + + ctx.hooks.games.fetchCollection.tapPromise(desc.name, async ({ source, id }) => + { + if (!await this.checkRemote()) return; + if (source !== 'romm') return; + const collection = await getCollectionApiCollectionsIdGet({ path: { id: Number(id) } }); + if (collection.data) + { + const col: FrontEndCollection = { + id: { source: 'romm', id: String(id) }, + name: collection.data.name, + description: collection.data.owner_username, + path_platform_cover: `/api/romm/image/romm${this.isSteamDeck ? collection.data.path_covers_small ?? collection.data.path_covers_small[0] : collection.data.path_cover_large ?? collection.data.path_covers_large[0]}`, + game_count: collection.data.rom_count + }; + return col; + } + + }); + + ctx.hooks.games.platformLookup.tapPromise(desc.name, async ({ source, id, slug }) => + { + if (!await this.checkRemote()) return; + let platform: PlatformSchema | undefined = undefined; + + if (id && source) + { + if (source !== 'romm') return; + const platforms = await this.getAllRommPlatforms(); + platform = platforms.find(p => p.id === Number(id)); + + } else if (slug) + { + const platforms = await this.getAllRommPlatforms(); + platform = platforms.find(p => p.slug === slug); + } + + if (!platform) return; + return { slug: platform?.slug, url_logo: platform.url_logo, name: platform.display_name, family_name: platform.family_name ?? undefined }; + }); + + ctx.hooks.games.searchGame.tapPromise(desc.name, async ({ source, igdb_id, ra_id }) => + { + if (!await this.checkRemote()) return; + if (source !== 'romm') return; + const roms = await getRomByMetadataProviderApiRomsByMetadataProviderGet({ query: { igdb_id, ra_id } }); + if (roms.error) throw roms.error; + if (!roms.data) return; + return this.convertRomToFrontendDetailed(roms.data); + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/package.json b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/package.json new file mode 100644 index 0000000..644c332 --- /dev/null +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/package.json @@ -0,0 +1,13 @@ +{ + "name": "com.simeonradivoev.gameflow.store", + "displayName": "Gameflow Store Integration", + "version": "0.0.1", + "description": "The internal gameflow store integration. This is the logic of the store that uses the data only store package", + "main": "./store.ts", + "category": "sources", + "canDisable": false, + "keywords": [ + "internal", + "store" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/services.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/services.ts new file mode 100644 index 0000000..4935dd7 --- /dev/null +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/services.ts @@ -0,0 +1,365 @@ +import { getStoreFolder } from "@/bun/api/store/services/gamesService"; +import os from 'node:os'; +import path from "node:path"; +import * as appSchema from '@schema/app'; +import * as emulatorSchema from '@schema/emulators'; +import { config, db, emulatorsDb, plugins } from "@/bun/api/app"; +import { and, eq } from "drizzle-orm"; +import { getOrCached } from "@/bun/api/cache"; +import { Glob } from "bun"; +import { shuffleInPlace } from "@/bun/utils"; +import mustache from "mustache"; +import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService"; +import fs from "node:fs/promises"; +import { CommandEntry, EmulatorSourceEntryType, EmulatorSystem, FrontEndEmulator, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailed, SaveFileChange, EmulatorDownloadInfoType, StoreDownloadType, StoreGameType, EmulatorPackageType, EmulatorDownloadInfoSchema, StoreGameSchema } from "@simeonradivoev/gameflow-sdk/shared"; +import { isUrl } from "@/shared/utils"; + +export async function getStoreGames (gamesManifest: any[], filter?: { limit?: number; offset?: number; }) +{ + const offset = filter?.offset ?? 0; + const limit = Math.min(50, filter?.limit ?? 10); + + const games = await Promise.all(gamesManifest.slice(offset, Math.min(offset + limit, gamesManifest.length)).map((e: any) => + { + return fetch(e.url).then(e => e.json()).then(game => StoreGameSchema.parseAsync(JSON.parse(atob(game.content.replace(/\n/g, ""))))); + })); + + return games; +} + +export async function getStoreGame (id: string) +{ + const file = Bun.file(path.join(getStoreFolder(), 'buckets', 'games', `${id}.json`)); + if (!(await file.exists())) return undefined; + const game = file + .json() + .then(g => StoreGameSchema.parseAsync(g)) + .then(g => ({ ...g, id })); + return game; +} + +function convertStoreMediaToPath (c: string) +{ + if (isUrl(c)) + { + return `/api/romm/image?url=${encodeURIComponent(c)}`; + } else + { + return `/api/store/media/${c}`; + } +} + +export async function convertStoreToFrontend (id: string, storeGame: StoreGameType): Promise +{ + const validDownloads = getValidDownloads(storeGame); + + let platform_slug: string | null = null; + let platform_id: number | null = null; + let platform_display_name: string | null = null; + let path_platform_cover: string | null = null; + + if (validDownloads.length > 0 && validDownloads[0].system) + { + let system = validDownloads[0].system.split(':')[0]; + if (system === 'win32') system = 'win'; + + const localPlatform = await db.query.platforms.findFirst({ where: eq(appSchema.platforms.slug, system), columns: { id: true, slug: true, name: true } }); + if (localPlatform) + { + platform_id = localPlatform.id; + platform_slug = localPlatform.slug; + path_platform_cover = `/api/romm/platform/local/${localPlatform.id}/cover`; + platform_display_name = localPlatform.name; + } + + if (platform_slug === null) + { + const rommSystem = await emulatorsDb.query.systemMappings.findFirst({ + where: and(eq(emulatorSchema.systemMappings.sourceSlug, system), eq(emulatorSchema.systemMappings.source, 'romm')) + }); + + if (rommSystem?.system) + { + const platformDef = await emulatorsDb.query.systems.findFirst({ + where: eq(emulatorSchema.systems.name, rommSystem?.system), + columns: { fullname: true } + }); + + platform_slug = rommSystem.system; + platform_display_name = platformDef?.fullname ?? null; + path_platform_cover = `/api/romm/image/romm/assets/platforms/${rommSystem.sourceSlug}.svg`; + + } else + { + const platformDef = await emulatorsDb.query.systems.findFirst({ + where: eq(emulatorSchema.systems.name, system), + columns: { fullname: true } + }); + + platform_slug = system; + platform_display_name = platformDef?.fullname ?? null; + } + + platform_slug ??= system; + } + } + + + const game: FrontEndGameType = { + platform_display_name, + path_platform_cover, + id: { source: 'store', id: id }, + source: null, + source_id: null, + path_fs: null, + path_covers: storeGame.covers?.map(convertStoreMediaToPath) ?? [], + last_played: null, + updated_at: new Date(), + slug: id, + name: storeGame.name, + platform_id, + platform_slug, + paths_screenshots: storeGame.screenshots?.map((s: string) => `/api/romm/image?url=${encodeURIComponent(s)}`) ?? [], + metadata: { + first_release_date: typeof storeGame.first_release_date === 'number' ? new Date(storeGame.first_release_date) : storeGame.first_release_date ?? null + } + }; + + return game; +} + + +export async function convertStoreToFrontendDetailed (id: string, storeGame: StoreGameType): Promise +{ + const validDownloads = getValidDownloads(storeGame); + let size: number | null = null; + if (validDownloads.length > 0 && validDownloads[0].url) + { + try + { + const fileResponse = await fetch(validDownloads[0]?.url, { method: 'HEAD' }); + size = Number(fileResponse.headers.get('content-length')); + } catch (error) + { + console.error(error); + } + } + + const detailed: FrontEndGameTypeDetailed = { + ...await convertStoreToFrontend(id, storeGame), + summary: storeGame.description, + fs_size_bytes: size, + missing: false, + local: false, + version: storeGame.version, + igdb_id: storeGame.igdb_id ?? null, + ra_id: storeGame.ra_id ?? null, + metadata: { + genres: storeGame.genres ?? [], + companies: storeGame.companies ?? [], + game_modes: [], + age_ratings: [], + player_count: storeGame.player_count ?? null, + average_rating: null, + first_release_date: typeof storeGame.first_release_date === 'number' ? new Date(storeGame.first_release_date) : storeGame.first_release_date ?? null + } + }; + + return detailed; +} + +export function getValidDownloads (game: StoreGameType, downloadId?: string) +{ + const downloads = Object.entries(game.downloads).map(([k, d]) => ({ id: k, ...d })); + const supportedDownloads = downloads.filter(d => d.type === 'direct'); + + if (downloadId) + { + return supportedDownloads.filter(d => d.id === downloadId); + } else + { + return supportedDownloads.filter(d => + { + if (d.system === `${process.platform}:${process.arch}`) return true; + + // TODO: Add linux proton support + //if (process.platform === 'linux' && d.system === `win32:${process.arch}`) return true; + + // emulator fallback + return !d.system.includes(':'); + }).toSorted((a, b) => + { + const bScore = b.system.includes(':') ? 0 : 1; + const aScore = a.system.includes(':') ? 0 : 1; + + return bScore - aScore; + }); + } +} + +export async function getShuffledStoreGames () +{ + return getOrCached('shuffled-store-games', async () => + { + const files = new Glob(path.join(getStoreFolder(), 'buckets', 'games', '*.json')).scan(); + const allGamePaths = await Array.fromAsync(files); + const allStoreGames = await Promise.all(allGamePaths.map(p => Bun.file(p).json().then(g => StoreGameSchema.parseAsync(g)).then(g => ({ ...g, id: path.basename(p, '.json') })))); + shuffleInPlace(allStoreGames, Math.round(new Date().getTime() / 1000 / 60 / 60)); + return allStoreGames; + }, { expireMs: 1000 / 60 / 60 }); +} + +export async function buildFilters (filters: FrontEndFilterSets) +{ + const filtersFile = Bun.file(path.join(getStoreFolder(), 'manifests', 'filters.json')); + if (!await filtersFile.exists()) return; + const storeFilters = await filtersFile.json(); + + storeFilters.genres?.forEach((g: string) => filters.genres.add(g)); + storeFilters.age_ratings?.forEach((g: string) => filters.age_ratings.add(g)); + if (storeFilters.player_count) + filters.player_counts.add(storeFilters.player_count); + storeFilters.companies?.forEach((g: string) => filters.companies.add(g)); +} + +function getAppData () +{ + if (process.platform === "win32") return process.env.APPDATA!; + if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support"); + // linux + return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"); +} + +function getLocalAppData () +{ + if (process.platform === "win32") return process.env.LOCALAPPDATA!; + if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Caches"); + // Linux / Unix + return process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"); +} + +export function buildSaves (command: CommandEntry, storeGame: StoreGameType, download?: StoreDownloadType) +{ + let saveFileGlobs: Record | undefined = undefined; + if (download && download.saves) + { + saveFileGlobs = download.saves; + + } else if (storeGame.saves) + { + const platformSaves = storeGame.saves[`${process.platform}:${process.arch}`]; + if (platformSaves) + { + saveFileGlobs = platformSaves; + } + } + + const view = { + GAMEDIR: command.startDir, + HOMEDIR: os.homedir(), + TMPDIR: os.tmpdir(), + APPDATA: getAppData(), + LOCALAPPDATA: getLocalAppData(), + }; + + if (!saveFileGlobs) return; + + return Object.entries(saveFileGlobs).map(([slot, save]) => + { + const cwd = mustache.render(save.cwd, view); + const change: SaveFileChange = { + cwd, + shared: false, + isGlob: true, + subPath: save.globs + }; + return [slot, change] as [string, SaveFileChange]; + }); +} + +export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageType, systems: EmulatorSystem[]) +{ + const execPaths: EmulatorSourceEntryType[] = []; + await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: emulator.name, sources: execPaths }); + + const em: FrontEndEmulator = { + name: emulator.name, + logo: emulator.logo, + systems, + gameCount: 0, + validSources: execPaths, + integrations: [], + source: "store" + }; + + return em; +} + +export async function getExistingStoreEmulatorDownload (emulator: EmulatorPackageType): Promise<(EmulatorDownloadInfoType & { hasUpdate: boolean; }) | undefined> +{ + const existingPackagePath = `${getEmulatorPath(emulator.name)}.json`; + if (await fs.exists(existingPackagePath)) + { + const existingPackage = await EmulatorDownloadInfoSchema.parseAsync(await Bun.file(existingPackagePath).json()); + const download = await getEmulatorDownload(emulator, existingPackage.type).catch(d => undefined); + if (!download) return { ...existingPackage, hasUpdate: false }; + if (download.info.version) + { + if (existingPackage.version !== download.info.version) return { ...existingPackage, hasUpdate: true }; + } else if (existingPackage.id !== download.info.id) + { + return { ...existingPackage, hasUpdate: true }; + } + + return { ...existingPackage, hasUpdate: false }; + } + + // this should only happen if download info is missing maybe manually deleted or wasn't saved. + return undefined; +} + +export async function buildLaunchCommand (ctx: { gamePath: string; systemSlug: string; mainGlob?: string | null; }): Promise +{ + if (ctx.systemSlug !== 'win' && ctx.systemSlug !== 'linux' && ctx.systemSlug !== 'mac') return; + const downloadPath = config.get('downloadPath'); + const gamePathAbsolute = path.join(downloadPath, ctx.gamePath); + if (!(await fs.exists(gamePathAbsolute))) return; + const gamePathStat = await fs.stat(gamePathAbsolute); + + if (gamePathStat.isDirectory()) + { + let mainGlob = ctx.mainGlob; + if (!mainGlob && ctx.systemSlug === 'win') mainGlob = '**/*.exe'; + if (!mainGlob) return; + const fileGlob = new Glob(mainGlob); + for await (const file of fileGlob.scan({ cwd: path.join(downloadPath, ctx.gamePath) })) + { + return { + startDir: path.join(downloadPath, ctx.gamePath, path.dirname(file)), + command: [`./${path.basename(file)}`], + id: `store-${process.platform}`, + shell: false, + valid: true, + metadata: { + romPath: path.join(downloadPath, ctx.gamePath, file) + } + }; + } + + } else + { + return { + startDir: path.join(downloadPath, path.dirname(ctx.gamePath)), + command: [`./${path.basename(ctx.gamePath)}`], + id: `store-${process.platform}`, + valid: true, + shell: false, + metadata: { + romPath: path.join(downloadPath, ctx.gamePath), + } + }; + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts new file mode 100644 index 0000000..92ce1f9 --- /dev/null +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts @@ -0,0 +1,459 @@ +import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import desc from './package.json'; +import path, { } from 'node:path'; +import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService"; +import { Glob, pathToFileURL, which } from "bun"; +import { and, eq } from "drizzle-orm"; +import * as emulatorSchema from '@schema/emulators'; + +import { config, emulatorsDb, taskQueue } from "@/bun/api/app"; +import fs from "node:fs/promises"; +import { getSourceGameDetailed } from "@/bun/api/games/services/utils"; +import EnsureStore from "@/bun/api/jobs/ensure-store"; +import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService"; +import { buildFilters, buildLaunchCommand, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownloads } from "./services"; +import { DownloadInfo, FrontEndEmulatorDetailed, FrontEndGameTypeWithIds } from "@simeonradivoev/gameflow-sdk/shared"; +import { isUrl } from "@/shared/utils"; +import { Downloader } from "@/bun/utils/downloader"; +import { ensureDir, move } from "fs-extra"; +import StreamZip from "node-stream-zip"; +import { path7za } from "7zip-bin"; +import Seven from 'node-7z'; + +export default class StoreIntegration implements PluginType +{ + eventsNames = [{ id: 'updateStore', title: "Update Store", description: "Update the Store Manifest", action: "Update" }]; + + async onEvent (e: string) + { + switch (e) + { + case 'updateStore': + await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); + return { reload: true }; + } + } + + async setup (ctx: PluginLoadingContextType) + { + console.log("Store Directory is ", getStoreFolder()); + ctx.setProgress(0, "Updating Store"); + await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); + } + + async load (ctx: PluginLoadingContextType) + { + await this.setup(ctx); + + ctx.hooks.store.fetchDownload.tapPromise(desc.name, async ({ id }) => + { + const emulatorPackage = await getStoreEmulatorPackage(id); + if (!emulatorPackage) return; + const downloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage); + return downloadInfo; + }); + + ctx.hooks.store.fetchEmulator.tapPromise(desc.name, async ({ id }) => + { + const emulatorPackage = await getStoreEmulatorPackage(id); + if (!emulatorPackage) return undefined; + + const systems = await buildStoreFrontendEmulatorSystems(emulatorPackage); + + const emulatorScreenshotsPath = path.join(getStoreFolder(), "media", "screenshots", id); + const screenshots = await fs.exists(emulatorScreenshotsPath) ? await fs.readdir(emulatorScreenshotsPath) : []; + const biosDirPath = path.join(config.get('downloadPath'), 'bios', id); + const biosFiles = await fs.exists(biosDirPath) ? await fs.readdir(biosDirPath) : []; + const storeDownloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage); + + const emulator: FrontEndEmulatorDetailed = { + name: emulatorPackage.name, + description: emulatorPackage.description, + source: "store", + systems, + validSources: [], + screenshots: screenshots.map(s => `/api/store/screenshot/emulator/${id}/${s}`), + gameCount: 0, + homepage: emulatorPackage.homepage, + downloads: (await Promise.all(emulatorPackage.downloads?.[`${process.platform}:${process.arch}`].map(async d => + { + const download = await getEmulatorDownload(emulatorPackage, d.type).catch(e => undefined); + return download?.info; + }) ?? [])).filter(d => !!d).map(d => ({ name: d.type, type: d.type, version: d.version })), + logo: emulatorPackage.logo, + biosRequirement: emulatorPackage.bios, + bios: biosFiles, + integrations: [], + storeDownloadInfo: storeDownloadInfo + }; + + return emulator; + }); + + ctx.hooks.store.fetchEmulators.tapPromise(desc.name, async ({ emulators, search }) => + { + const emulatesParsed = await getAllStoreEmulatorPackages(); + emulators.push(...await Promise.all(emulatesParsed + .filter(e => + { + if (!e.os.includes(process.platform as any)) return false; + if (search) + { + if (e.name.toLocaleLowerCase().includes(search) || e.systems.some(s => s.toLocaleLowerCase().includes(search)) || e.keywords?.some(k => k.toLocaleLowerCase().includes(search))) + { + return true; + } + + return false; + } + return true; + }) + .map(async (emulator) => + { + const systems = await buildStoreFrontendEmulatorSystems(emulator); + return convertStoreEmulatorToFrontend(emulator, systems); + }))); + }); + + ctx.hooks.games.prePlay.tapPromise(desc.name, async ({ source, id, saveFolderSlots, command }) => + { + if (source !== 'store') return; + const storeGame = await getStoreGame(id); + const localGame = await getSourceGameDetailed(source, id); + + if (!localGame || !storeGame) return; + if (!localGame.version_source) return; + + const download = storeGame.downloads[localGame.version_source]; + const saves = buildSaves(command, storeGame, download); + + saves?.forEach(([slot, save]) => saveFolderSlots[slot] = { cwd: save.cwd }); + }); + + ctx.hooks.games.postPlay.tapPromise(desc.name, async ({ validChangedSaveFiles, source, id, command }) => + { + if (source !== 'store') return; + const storeGame = await getStoreGame(id); + const localGame = await getSourceGameDetailed(source, id); + + if (!localGame || !storeGame) return; + if (!localGame.version_source) return; + + const download = storeGame.downloads[localGame.version_source]; + + const saves = buildSaves(command, storeGame, download); + saves?.forEach(([key, val]) => validChangedSaveFiles[key] = val); + }); + + ctx.hooks.emulators.findEmulatorSource.tapPromise(desc.name, async ({ emulator, sources }) => + { + const emulatorPackage = await getStoreEmulatorPackage(emulator); + if (!emulatorPackage) return undefined; + const storeDownloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage); + if (!storeDownloadInfo) return; + const emulatorPath = getEmulatorPath(emulator); + if (!await fs.exists(emulatorPath)) return; + const validDownload = emulatorPackage.downloads?.[`${process.platform}:${process.arch}`].find(d => d.type === storeDownloadInfo?.type); + if (!validDownload || !validDownload.bin) return; + const glob = new Glob(validDownload.bin); + const files = await Array.fromAsync(glob.scan({ cwd: emulatorPath })); + // es-de also searches for store executables so there might be duplicates, check first. + if (files.length > 0 && !sources.find(s => s.type === 'store')) + { + sources.push({ binPath: path.join(emulatorPath, files[0]), exists: true, rootPath: emulatorPath, type: 'store' }); + } + }); + + ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: 'UMU' }, async ({ path: emulatorPath }) => + { + const pathStat = await fs.stat(emulatorPath); + if (pathStat.isFile()) + { + await fs.chmod(emulatorPath, 0o755); + } + }); + + ctx.hooks.games.postInstall.tapPromise(desc.name, async ({ source, id, files, info }) => + { + if (source !== 'store') return; + if (files.length === 1) + { + const command = await buildLaunchCommand({ gamePath: files[0], systemSlug: info.system_slug, mainGlob: info.main_glob }); + if (command && command.metadata.romPath) + { + await fs.chmod(command.metadata.romPath, 0o755); + } + } + }); + + ctx.hooks.games.buildLaunchCommands.tapPromise({ name: desc.name, before: 'com.simeonradivoev.gameflow.es' }, async ({ gamePath, source, sourceId, systemSlug, mainGlob }) => + { + if (source !== 'store' || !gamePath) return; + const command = await buildLaunchCommand({ gamePath, systemSlug, mainGlob }); + if (!command) return; + return [command]; + }); + + ctx.hooks.games.fetchFilters.tapPromise(desc.name, async ({ filters, source }) => + { + if (!source || source !== 'store') return; + await buildFilters(filters); + }); + + ctx.hooks.store.fetchFeaturedGames.tapPromise(desc.name, async ({ games }) => + { + const allGames = await getShuffledStoreGames(); + const convertedGames = await Promise.all(allGames.slice(0, 3).map(async g => + { + return convertStoreToFrontendDetailed(g.id, g); + })); + games.push(...convertedGames); + }); + + ctx.hooks.games.fetchGames.tapPromise(desc.name, async ({ query, games }) => + { + if (!query.source || query.source !== 'store') return; + if (query.collection_source || query.collection_id) return; + + const shuffledGames = await getShuffledStoreGames(); + const storeGames = await Promise.all(shuffledGames.filter(g => + { + if (query.search) + return path.basename(g.name).toLocaleLowerCase().includes(query.search.toLocaleLowerCase()); + return true; + }) + .slice(query.offset ?? 0, Math.min((query.offset ?? 0) + (query.limit ?? 50), shuffledGames.length)) + .map(async (e) => + { + const game: FrontEndGameTypeWithIds = { + ...await convertStoreToFrontend(e.id, e), + igdb_id: e.igdb_id ?? null, + ra_id: e.ra_id ?? null + }; + return game; + })); + games.push(...storeGames.filter(g => g !== undefined)); + }); + + ctx.hooks.games.fetchRecommendedGamesForGame.tapPromise(desc.name, async ({ game, games }) => + { + const esSystem = game.platform_slug ? await emulatorsDb.query.systemMappings.findFirst({ where: and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.sourceSlug, game.platform_slug)), columns: { system: true } }) : undefined; + + const shuffledGames = await getShuffledStoreGames(); + const storeGames = await Promise.all(shuffledGames + .filter(g => + { + if (esSystem) + { + if (Object.values(g.downloads).some(d => d.system === esSystem.system)) return true; + } + + return false; + }) + .map(async (e) => + { + return convertStoreToFrontend(e.id, e); + })); + + if (storeGames) + { + games.push(...storeGames.slice(0, 3)); + } + }); + + ctx.hooks.games.fetchRecommendedGamesForEmulator.tapPromise(desc.name, async ({ emulator, games, systems }) => + { + const systemsIdSet = new Set(systems.map(s => s.id)); + const gamesManifest = await getShuffledStoreGames(); + const storeGames = await Promise.all(gamesManifest + .filter(g => Object.values(g.downloads).some(d => systemsIdSet.has(d.system))) + .map(async (e) => + { + + return convertStoreToFrontend(e.id, e); + })); + + games.push(...storeGames.filter(g => g !== undefined).slice(0, 3)); + }); + + ctx.hooks.games.fetchGame.tapPromise(desc.name, async ({ source, id }) => + { + if (source !== 'store') return; + const storeGame = await getStoreGame(id); + if (storeGame) + { + return convertStoreToFrontendDetailed(id, storeGame); + } + }); + + ctx.hooks.games.fetchDownloads.tapPromise(desc.name, async ({ source, id, downloadId }) => + { + if (source !== 'store') return; + const game = await getStoreGame(id); + if (!game) throw new Error("Missing Store Game"); + + const validDownloads = getValidDownloads(game, downloadId); + + return validDownloads.map(validDownload => + { + let system = validDownload.system.split(":")[0]; + if (system === 'win32') system = 'win'; + + const info: DownloadInfo = { + id: validDownload.id, + coverUrl: game.covers?.[0] ? isUrl(game.covers[0]) ? game.covers[0] : pathToFileURL(path.join(getStoreFolder(), game.covers[0])).href : "", + screenshotUrls: game.screenshots ?? [], + files: [{ + url: new URL(validDownload.url), + file_path: `roms/${system}`, + file_name: path.basename(decodeURI(validDownload.url)), + size: 0 + }], + slug: id, + source_id: id, + name: game.name, + summary: game.description, + system_slug: system, + path_fs: path.join('roms', system, game.id), + extract_path: '.', + main_glob: validDownload.main, + version: game.version, + version_system: validDownload.system, + version_source: validDownload.id, + platform: { + source: 'store', + id: system, + slug: system, + name: system + } + }; + + return info; + }); + }); + + ctx.hooks.downloadFiles.tapPromise(desc.name, async ({ id, files, downloadPath, abortSignal, auth, updateProgress }) => + { + const headers: Record = {}; + if (auth) + headers['Authorization'] = auth; + const downloader = new Downloader(id, + files, + downloadPath, + { + signal: abortSignal, + headers, + onProgress: updateProgress, + }); + + const downloadedFiles = await downloader.start(); + if (downloadedFiles) + { + return { source: desc.name, files: downloadedFiles }; + } + }); + + ctx.hooks.postDownloadFiles.tapPromise(desc.name, async ({ files, extract_path, source, downloadPath, path_fs }) => + { + if (extract_path && files && source === desc.name) + { + let progress = 0; + const progressDelta = 1 / files.length; + const extractPath = path.join(downloadPath, path_fs ?? '', extract_path); + + for (const filePath of files) + { + await new Promise(async (resolve, reject) => + { + let sevenZipPath = process.env.ZIP7_PATH ?? path7za; + + if (filePath.endsWith('.rar')) + { + let newPath: string | undefined; + if (process.platform === 'win32' && await fs.exists("C:\\Program Files\\7-Zip\\7z.exe")) + { + newPath = "C:\\Program Files\\7-Zip\\7z.exe"; + } else + { + newPath = which('7z') ?? undefined; + } + + if (!newPath) + { + await fs.rm(filePath); + reject(new Error("No RAR Support")); + return; + } + + sevenZipPath = newPath; + } + + let rejected = false; + const seven = Seven.extractFull(filePath, extractPath, { $bin: sevenZipPath, $progress: true }); + seven.on('progress', p => + { + ctx.setProgress?.(progress + p.percent * progressDelta, "extract", { + speed: 0, + total: 0, + downloaded: 0 + }); + }); + seven.on('error', e => + { + reject(e); + rejected = true; + }); + seven.on('end', async () => + { + if (rejected) return; + await fs.rm(filePath); + resolve(true); + }); + }).catch(async e => + { + if (filePath.endsWith('.zip')) + { + ctx.setProgress?.(0, "extract", {}); + console.error(e); + console.warn("Could not extract", filePath, "with 7zip trying zip extractor"); + await ensureDir(extractPath); + const zip = new StreamZip.async({ file: filePath }); + let entryCount = await zip.entriesCount; + let entryCounter = entryCount; + zip.on('extract', (entry, outPath) => + { + entryCounter--; + ctx.setProgress?.(progress + (1 - (entryCounter / entryCount)) * 100 * progressDelta, "extract", {}); + }); + const count = await zip.extract(null, extractPath); + console.log(`Extracted ${count} entries`); + await zip.close(); + await fs.rm(filePath); + } else + { + throw e; + } + }); + + progress += progressDelta * 100; + } + + // check if 1 root folder we need to get rid of + const contents = await fs.readdir(extractPath); + if (contents.length === 1) + { + const stat = await fs.stat(path.join(extractPath, contents[0])); + if (stat.isDirectory()) + { + console.log("Found 1 root folder, using that instead"); + const tmpGameFolder = `${extractPath} (1)`; + await move(path.join(extractPath, contents[0]), tmpGameFolder, { overwrite: true }); + await move(tmpGameFolder, extractPath, { overwrite: true }); + } + } + + return [extractPath]; + } + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/plugin-manager.ts b/src/bun/api/plugins/plugin-manager.ts new file mode 100644 index 0000000..8b2a1b5 --- /dev/null +++ b/src/bun/api/plugins/plugin-manager.ts @@ -0,0 +1,175 @@ +import { GameflowHooks } from "@simeonradivoev/gameflow-sdk"; +import { PluginDescriptionType, PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import { config, events, taskQueue } from "../app"; +import Conf from "conf"; +import projectPackage from '~/package.json'; +import z from "zod"; +import { PluginSourceType, PluginUpdateCheck } from "@simeonradivoev/gameflow-sdk/shared"; +import { getUpdates } from "./services"; +import sdkPkg from '@simeonradivoev/gameflow-sdk/package.json'; +import { semver } from "bun"; + +export const pluginZodRegistry = z.registry<{ + requiresRestart?: boolean; + readOnly?: boolean; +}>(); + +export class PluginManager +{ + hooks = new GameflowHooks(); + plugins: Record = {}; + + unregister (id: string) + { + if (!this.plugins[id]) return false; + delete this.plugins[id]; + console.log("Plugin", id, "unregistered"); + return true; + } + + register (plugin: PluginType, description: PluginDescriptionType, source: PluginSourceType) + { + try + { + if (this.plugins[description.name]) + { + console.error("Plugin with name", description.name, "already registered"); + } + else + { + let pluginConfig: Conf | undefined = undefined; + if (plugin.settingsSchema) + { + pluginConfig = new Conf({ + projectName: projectPackage.name, + configName: description.name, + projectSuffix: 'bun', + cwd: process.env.CONFIG_CWD, + schema: Object.fromEntries(Object.entries(plugin.settingsSchema.shape).map(([key, schema]) => [key, (schema as z.ZodObject).toJSONSchema() as any])) as any, + defaults: plugin.settingsSchema.parse({}), + migrations: plugin.settingsMigrations as any, + projectVersion: description.version + }); + } + + this.plugins[description.name] = { + enabled: !config.get('disabledPlugins').includes(description.name), + loaded: false, + plugin: plugin, + source: source, + description: description, + config: pluginConfig + }; + console.log("Plugin", description.name, "registered"); + } + + } + catch (error) + { + console.log("Error While Registering plugin"); + console.error(error); + }; + } + + checkValidity (plugin: PluginDescriptionType) + { + const sdkDep = plugin.peerDependencies?.[sdkPkg.name]; + if (sdkDep) + { + return semver.satisfies(sdkPkg.version, sdkDep); + } + return true; + } + + private async reload (name: string, reloadCtx: { setProgress: (progress: number, state: string) => void; }, update: string | undefined | null) + { + const plugin = this.plugins[name]; + if (plugin) + { + plugin.update = update && !semver.satisfies(plugin.description.version, update) ? { current: plugin.description.version, new: update } : undefined; + + const ctx: PluginLoadingContextType = { + hooks: this.hooks, + setProgress: reloadCtx.setProgress.bind(reloadCtx), + config: plugin.config as any, + zodRegistry: pluginZodRegistry, + app: { + config, + events, + taskQueue + } + }; + + if (plugin.loaded) + { + await plugin.plugin.cleanup?.(); + plugin.loaded = false; + } + + try + { + plugin.incompatible = !this.checkValidity(plugin.description); + if (plugin.incompatible) + { + console.error(plugin.description.name, "Incompatible sdk verison"); + return; + } + + if (plugin.enabled || plugin.description.canDisable === false || plugin.description.name === '@simeonradivoev/gameflow-store') + { + console.log("Loading Plugin", plugin.description.name); + await plugin.plugin.load(ctx); + console.log("Loaded Plugin", plugin.description.name); + plugin.loaded = true; + } + } catch (error) + { + console.log("Error for plugin", plugin.description.name, "while loading"); + console.error(error); + } + } + } + + async reloadAll (ctx: { setProgress: (progress: number, state: string) => void; }) + { + this.hooks = new GameflowHooks(); + + const outdated = await getUpdates(); + + for await (const id of Object.keys(this.plugins)) + { + ctx.setProgress(0, `Loading ${id}`); + await this.reload(id, ctx, outdated.find(i => i.package === id)?.update); + } + } + + async cleanup () + { + await Promise.all(Object.values(this.plugins).filter(p => p.loaded && p.plugin.cleanup).map(async p => + { + try + { + if (p.loaded) + { + console.log("Starting", p.description.name, "plugin cleanup"); + await p.plugin.cleanup!(); + console.log(p.description.name, "cleanup complete"); + } + } catch (error) + { + console.error("Error for plugin", p.description.name, "while cleaning up"); + console.error(error); + } + })); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/plugins.ts b/src/bun/api/plugins/plugins.ts new file mode 100644 index 0000000..ddfad06 --- /dev/null +++ b/src/bun/api/plugins/plugins.ts @@ -0,0 +1,77 @@ +import Elysia, { status } from "elysia"; +import { plugins, taskQueue } from "../app"; +import z from "zod"; +import { toggleElementInConfig } from "@/bun/utils"; +import ReloadPluginsJob from "../jobs/reload-plugins-job"; +import { FrontendPlugin } from "@simeonradivoev/gameflow-sdk/shared"; +import { canDisable, canUninstall } from "./services"; +import PluginOperationJob from "../jobs/plugin-operation-job"; + +export default new Elysia({ prefix: '/plugins' }) + .get('/', async () => + { + return Object.values(plugins.plugins).map(p => + { + const plugin: FrontendPlugin = { + enabled: p.enabled, + name: p.description.name, + displayName: p.description.displayName, + description: p.description.description, + source: p.source, + version: p.description.version, + canDisable: canDisable(p.description), + icon: p.description.icon, + category: p.description.category, + hasSettings: !!p.config || !!p.plugin.eventsNames, + canUninstall: canUninstall(p.description, p.source), + update: p.update + }; + return plugin; + }); + }) + .get('/:id', async ({ params: { id } }) => + { + const plugin = plugins.plugins[decodeURIComponent(id)]; + return { ...plugin.description, update: plugin.update }; + }) + .post('/:id', async ({ params: { id }, body: { enabled } }) => + { + const plugin = plugins.plugins[decodeURIComponent(id)]; + if (plugin) + { + if (!canDisable(plugin.description)) + { + return status("Forbidden"); + } + plugin.enabled = enabled; + toggleElementInConfig('disabledPlugins', plugin.description.name, enabled); + await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); + } else + { + return status("Not Found"); + } + }, { + body: z.object({ enabled: z.boolean() }) + }).post('/install', async ({ body: { id } }) => + { + if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return; + await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("add", id)); + await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); + }, { + body: z.object({ id: z.string() }) + }).post('/update', async ({ body: { id } }) => + { + if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return; + await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("update", id)); + await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); + }, { + body: z.object({ id: z.string() }) + }) + .post('/uninstall', async ({ body: { id } }) => + { + if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return; + await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("remove", id)); + await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); + }, { + body: z.object({ id: z.string() }) + }); \ No newline at end of file diff --git a/src/bun/api/plugins/register-plugins.ts b/src/bun/api/plugins/register-plugins.ts new file mode 100644 index 0000000..5746947 --- /dev/null +++ b/src/bun/api/plugins/register-plugins.ts @@ -0,0 +1,153 @@ +import { PluginManager } from "./plugin-manager"; + +import pcsx2 from './builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json'; +import ppsspp from './builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json'; +import dolphin from './builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json'; +import cemu from './builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json'; +import xenia from './builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json'; +import xemu from './builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json'; +import romm from './builtin/sources/com.simeonradivoev.gameflow.romm/package.json'; +import igdb from './builtin/sources/com.simeonradivoev.gameflow.igdb/package.json'; +import store from './builtin/sources/com.simeonradivoev.gameflow.store/package.json'; +import es from './builtin/launchers/com.simeonradivoev.gameflow.es/package.json'; +import rclone from './builtin/other/com.simeonradivoev.gameflow.rclone/package.json'; +import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@simeonradivoev/gameflow-sdk"; +import path from 'node:path'; +import { getStoreRootFolder } from "../store/services/gamesService"; +import { getUpdates, runBunPackageCommand } from "./services"; +import { PluginSourceType } from "@simeonradivoev/gameflow-sdk/shared"; +import { taskQueue } from "../app"; +import EnsureStore from "../jobs/ensure-store"; +import { PluginRegistry } from "@/shared/constants"; +import { IsPluginAllowed } from "@/bun/utils"; + +type PluginEntry = PluginDescriptionType & { load: () => Promise; }; + +const blacklist = new Set(['@simeonradivoev/gameflow-sdk']); + +export async function getPlugin (id: string, pluginManager: PluginManager) +{ + const pluginPath = path.join(getStoreRootFolder(), 'node_modules', id); + const pluginPackageFile = Bun.file(path.join(pluginPath, 'package.json')); + if (await pluginPackageFile.exists()) + { + const pluginPackage = await PluginDescriptionSchema.safeParseAsync(await pluginPackageFile.json()); + if (pluginPackage.success) + { + const mainPath = path.join(pluginPath, pluginPackage.data.main); + if (await Bun.file(mainPath).exists()) + { + const entry: PluginEntry = { ...pluginPackage.data, load: () => import(mainPath) }; + return entry; + } else + { + console.error("Main file for", id, "does not exist"); + } + } else + { + console.error("Invalid Package for", id, pluginPackage.error.message); + } + } else + { + console.error("Package for", id, "does not exist"); + } +} + +export async function unregisterPlugin (id: string, pluginManager: PluginManager) +{ + return pluginManager.unregister(id); +} + +export async function registerPlugin (plugin: PluginEntry, source: PluginSourceType, pluginManager: PluginManager) +{ + if (!IsPluginAllowed(plugin.name)) + { + console.log("Skipping", plugin.name, "plugin not allowed"); + return; + } + + const file = await plugin.load(); + if (file.default && typeof file.default === 'function') + { + const pluginInstance = new file.default(); + await PluginSchema.parseAsync(pluginInstance); + const description = await PluginDescriptionSchema.parseAsync(plugin); + pluginManager.register(pluginInstance, description, source); + } else + { + console.log("Skipping", plugin.name, "invalid main. Has to be class with load method"); + } +} + +export default async function register (pluginManager: PluginManager) +{ + const plugins: PluginEntry[] = [ + { ...pcsx2, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2') }, + { ...ppsspp, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp') }, + { ...dolphin, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin') }, + { ...cemu, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu') }, + { ...xenia, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia') }, + { ...xemu, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu') }, + { ...romm, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.romm/romm') }, + { ...igdb, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.igdb/igdb') }, + { ...es, load: () => import('./builtin/launchers/com.simeonradivoev.gameflow.es/es-de') }, + { ...store, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.store/store') }, + { ...rclone, load: () => import('./builtin/other/com.simeonradivoev.gameflow.rclone/rclone') }, + ]; + + await Promise.all(plugins.map(p => registerPlugin(p, 'builtin', pluginManager))); + + if (IsPluginAllowed('@simeonradivoev/gameflow-store')) + { + const storePackageFilePath = path.join(getStoreRootFolder(), 'package.json'); + if (!await Bun.file(storePackageFilePath).exists()) + { + console.log("Store is missing. Updating it."); + await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); + console.log("Store Updated"); + } + const storePackage = await Bun.file(storePackageFilePath).json(); + + if (storePackage?.dependencies) + { + const storePlugins = await Promise.all(Object.keys(storePackage.dependencies).filter(p => !blacklist.has(p)).map(async p => + { + return getPlugin(p, pluginManager); + })); + + console.log("Checking for outdated packages"); + const outdated = await getUpdates(); + + const validPlugins = storePlugins.filter(p => !!p); + + if (outdated) + { + for (let i = 0; i < validPlugins.length; i++) + { + const plugin = validPlugins[i]; + const newVersion = outdated.find(i => i.package === plugin.name); + if (newVersion) + { + console.log("Plugin", plugin.name, "has update", plugin.version, "=>", newVersion.update); + + if (plugin.autoUpdate || plugin.name === '@simeonradivoev/gameflow-store') + { + console.log("Auto Updating Plugin", plugin.name); + let response = await runBunPackageCommand(["add", `${plugin.name}@${newVersion?.update}`, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + // Update plugin package + const newPlugin = await getPlugin(plugin.name, pluginManager); + if (newPlugin) + validPlugins[i] = newPlugin; + } + } + } + } + + await Promise.all(validPlugins.map(p => registerPlugin(p, 'store', pluginManager))); + } + } else + { + console.log('Skipping Store Packages'); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/services.ts b/src/bun/api/plugins/services.ts new file mode 100644 index 0000000..8878809 --- /dev/null +++ b/src/bun/api/plugins/services.ts @@ -0,0 +1,64 @@ +import path from 'node:path'; +import os from 'node:os'; +import { getStoreRootFolder } from '../store/services/gamesService'; +import { PluginDescriptionType } from '@simeonradivoev/gameflow-sdk'; +import { existsSync } from 'node:fs'; +import { checkOutdated } from './update-check'; + +export function canDisable (description: PluginDescriptionType) +{ + if (description.name === '@simeonradivoev/gameflow-store') + { + return false; + } + return description.canDisable ?? true; +} + +export async function getUpdates () +{ + if (!existsSync(getStoreRootFolder())) return []; + const results = await checkOutdated(getStoreRootFolder()); + return results; +} + +export function canUninstall (description: PluginDescriptionType, source: string) +{ + if (description.name === '@simeonradivoev/gameflow-store') + { + return false; + } + return source !== 'builtin'; +} + +export async function runBunPackageCommand (commands: string[]) +{ + const tempCache = path.join(os.tmpdir(), "gameflow-bun-cache"); + const storeFolder = getStoreRootFolder(); + + let proc = Bun.spawn([process.execPath, ...commands, '--json'], { + cwd: storeFolder, + stdout: 'pipe', + stderr: 'pipe', + env: { + BUN_BE_BUN: "1", + BUN_INSTALL_CACHE_DIR: tempCache + } + }); + + let stdout = await new Response(proc.stdout).text(); + let stderr = await new Response(proc.stderr).text(); + if (stderr) + console.error(stderr); + await proc.exited; + return stdout; +} + +export async function hasPackage (id: string) +{ + const storeFolder = getStoreRootFolder(); + const packagePath = path.join(storeFolder, 'package.json'); + const packageFile = Bun.file(packagePath); + if (!await packageFile.exists()) return false; + const pkg = await packageFile.json(); + return !!pkg.dependencies?.[id]; +} \ No newline at end of file diff --git a/src/bun/api/plugins/update-check.ts b/src/bun/api/plugins/update-check.ts new file mode 100644 index 0000000..66cf381 --- /dev/null +++ b/src/bun/api/plugins/update-check.ts @@ -0,0 +1,169 @@ +import { semver } from "bun"; +import { readFile } from "fs/promises"; +import { join } from "path"; +import { getOrCached } from "../cache"; +import { PluginRegistry } from "@/shared/constants"; +import sdkPkg from '@/packages/gameflow-sdk/package.json'; + +interface UpdateInfo +{ + package: string, + current: string, + update: string | null, + latest: string, + sdkConstrained: boolean, + sdkRange: string, + note: string | null; +} + +function parseBunOutdated (cwd: string) +{ + const proc = Bun.spawnSync([process.execPath, "outdated"], { + stderr: "inherit", env: { + BUN_BE_BUN: "1", + NO_COLOR: "1", + }, cwd: cwd + }); + const output = proc.stdout.toString(); + const lines = output.split("\n").filter(Boolean); + + const headerIndex = lines.findIndex( + (l) => l.includes("Package") && l.includes("Current") + ); + if (headerIndex === -1) return []; + + return lines + .slice(headerIndex + 1) + .filter((line) => !/^[-─╌| ]+$/.test(line)) + .map((line) => + { + const [, pkg, current, , latest] = line.split("|").map((c) => c.trim()); + return pkg ? { package: pkg, current, latest } : null; + }) + .filter(p => p !== null); +} + +async function getInstalledVersion (cwd: string, pkg: string) +{ + try + { + const raw = await readFile(join(cwd, "node_modules", pkg, "package.json"), "utf8"); + return JSON.parse(raw).version ?? null; + } catch + { + return null; + } +} + +async function fetchAllVersions (pkg: string) +{ + const res = await fetch(`${PluginRegistry}/${pkg}`); + if (!res.ok) return []; + const data = await res.json(); + return Object.keys(data.versions ?? {}); +} + +async function fetchPeerDeps (pkg: string, version: string) +{ + const peerDependencies = await getOrCached(`npm-${pkg}-${version}`, async () => + { + const res = await fetch(`${PluginRegistry}/${pkg}/${version}`); + if (!res.ok) + { + throw new Error(`Error while fetching peer deps for ${pkg} ${version} ${res.status} ${res.statusText}`); + } + const data = await res.json(); + return data.peerDependencies ?? {}; + }, { + //5 days + expireMs: 1000 * 60 * 60 * 24 * 5 + }); + + + return peerDependencies; +} + +async function findBestVersion (pkg: string, allVersions: string[], sdkVersion: string) +{ + // Sort descending so we find the highest compatible version first + const sorted = [...allVersions].sort((a, b) => semver.order(b, a)); + + for (const version of sorted) + { + const peers = await fetchPeerDeps(pkg, version); + const sdkRange = peers[sdkPkg.name]; + + if (!sdkRange) + { + // No peer dep on SDK — compatible by default + return { version, sdkRange: null }; + } + + if (semver.satisfies(sdkVersion, sdkRange)) + { + return { version, sdkRange }; + } + } + + return null; +} + +export async function checkOutdated (cwd: string) +{ + const outdated = parseBunOutdated(cwd); + + if (outdated.length === 0) + { + return []; + } + + const sdkVersion = await getInstalledVersion(cwd, sdkPkg.name); + if (!sdkVersion) + { + console.error(`Could not find installed version of ${sdkPkg.name} in node_modules.`); + process.exit(1); + } + + const results = await Promise.all( + outdated.map(async ({ package: pkg, current, latest }) => + { + const allVersions = await fetchAllVersions(pkg); + + // Check if the outright latest is already SDK compatible + const latestPeers = await fetchPeerDeps(pkg, latest); + const latestSdkRange = latestPeers[sdkPkg.name]; + + const latestCompatible = + !latestSdkRange || semver.satisfies(sdkVersion, latestSdkRange); + + if (latestCompatible) + { + return { + package: pkg, + current, + update: latest, + latest, + sdkConstrained: false, + sdkRange: latestSdkRange ?? null, + note: null + } satisfies UpdateInfo as UpdateInfo; + } + + const best = await findBestVersion(pkg, allVersions, sdkVersion); + + return { + package: pkg, + current, + update: best?.version ?? null, + latest, + sdkConstrained: true, + sdkRange: best?.sdkRange ?? null, + note: best + ? `Latest (${latest}) requires incompatible SDK range; best compatible: ${best.version}` + : `No version of ${pkg} is compatible with ${sdkPkg.name}@${sdkVersion}`, + } satisfies UpdateInfo as UpdateInfo; + }) + ); + + return results; +} \ No newline at end of file diff --git a/src/bun/api/rpc.ts b/src/bun/api/rpc.ts index 6ad55d3..c7e9bac 100644 --- a/src/bun/api/rpc.ts +++ b/src/bun/api/rpc.ts @@ -7,28 +7,42 @@ import { system } from "./system"; import { store } from "./store/store"; import { host } from "../utils/host"; import { jobs } from "./jobs/jobs"; +import plugins from "./plugins/plugins"; -const api = new Elysia({ serve: {} }) - .use([cors(), clients, settings, system, store, jobs]); +const api = new Elysia() + .use([cors(), clients, settings, system, store, jobs, plugins]); export type RommAPIType = typeof clients; export type SettingsAPIType = typeof settings; export type SystemAPIType = typeof system; export type StoreAPIType = typeof store; export type JobsAPIType = typeof jobs; +export type PluginsAPIType = typeof plugins; -export function RunAPIServer () +export async function RunAPIServer () { - console.log("Launching API Server on port ", RPC_PORT); - return { - apiServer: api.listen({ + await new Promise((resolve, reject) => + { + const timeout = setTimeout(() => reject(new Error("Server startup timed out")), 5000); + + api.listen({ port: RPC_PORT, - hostname: host, + ...(host && host !== 'localhost' && { hostname: host }), development: process.env.NODE_ENV === 'development' - }), + }, s => + { + clearTimeout(timeout); + console.log("Launching API Server on", s.url.href); + resolve(); + }); + }); + + await api.modules; + return { + apiServer: api, async cleanup () { - + await api.stop(true); } }; } \ No newline at end of file diff --git a/src/bun/api/schema/app.ts b/src/bun/api/schema/app.ts index fafa32a..7db68ba 100644 --- a/src/bun/api/schema/app.ts +++ b/src/bun/api/schema/app.ts @@ -1,3 +1,5 @@ + +import { LocalGameMetadata } from "@simeonradivoev/gameflow-sdk/shared"; import { sql, relations } from "drizzle-orm"; import { integer, text, sqliteTable, blob } from "drizzle-orm/sqlite-core"; @@ -9,14 +11,18 @@ export const games = sqliteTable('games', { name: text("name"), ra_id: integer('ra_id').unique(), path_fs: text("path_fs"), + main_glob: text("main_glob"), last_played: integer("last_played", { mode: 'timestamp' }), created_at: integer("created_at", { mode: 'timestamp' }).default(sql`(unixepoch())`).notNull(), - metadata: text("metadata", { mode: 'json' }).default(sql`'{}'`), + metadata: text("metadata", { mode: 'json' }).default(sql`'{}'`).$type().notNull(), slug: text("slug").unique(), platform_id: integer("platform_id").references(() => platforms.id, { onUpdate: 'cascade' }).notNull(), cover: blob("cover", { mode: 'buffer' }), cover_type: text('type'), - summary: text("summary") + summary: text("summary"), + version: text('version'), + version_source: text("version_source"), + version_system: text("version_system"), }); export const gamesRelations = relations(games, ({ many, one }) => ({ diff --git a/src/bun/api/schema/emulators.ts b/src/bun/api/schema/emulators.ts index 0af0ff0..c70679d 100644 --- a/src/bun/api/schema/emulators.ts +++ b/src/bun/api/schema/emulators.ts @@ -3,6 +3,7 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const emulators = sqliteTable('emulators', { name: text().primaryKey().unique(), + fullname: text(), systempath: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`), staticpath: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`), corepath: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`), diff --git a/src/bun/api/settings/services.ts b/src/bun/api/settings/services.ts index cce32de..e0897ea 100644 --- a/src/bun/api/settings/services.ts +++ b/src/bun/api/settings/services.ts @@ -1,11 +1,13 @@ import * as appSchema from '@schema/app'; -import { findExec, findExecByName } from "../games/services/launchGameService"; import * as emulatorSchema from "@schema/emulators"; import { eq, inArray } from 'drizzle-orm'; -import { customEmulators, db, emulatorsDb } from '../app'; -import fs from 'node:fs/promises'; +import { db, emulatorsDb, plugins } from '../app'; import { cores } from '../emulatorjs/emulatorjs'; +import { SERVER_URL } from '@/shared/constants'; +import { host } from '@/bun/utils/host'; +import { findEmulatorPluginIntegration } from '../store/services/emulatorsService'; +import { EmulatorSourceEntryType, FrontEndEmulator } from '@simeonradivoev/gameflow-sdk/shared'; /** * Get emulators based on local games. Only the ones we probably need. @@ -52,14 +54,9 @@ export async function getRelevantEmulators () const groupedEmulators = Map.groupBy(emulators, ({ emulator }) => emulator); const finalEmulators = await Promise.all(Array.from(groupedEmulators.entries()).map(async ([emulator, system_slug]) => { - let execPath: { path: string; type: string, } | undefined; - if (customEmulators.has(emulator)) - { - execPath = { path: customEmulators.get(emulator), type: 'custom' }; - } else - { - execPath = await findExecByName(emulator); - } + const execPaths: EmulatorSourceEntryType[] = []; + await plugins.hooks.emulators.findEmulatorSource.promise({ emulator, sources: execPaths }); + const integrations = findEmulatorPluginIntegration(emulator, execPaths); let platform: number | null | undefined = null; const validSystemSlug = system_slug.find(s => s.system); @@ -67,127 +64,50 @@ export async function getRelevantEmulators () { platform = platformLookup.get(validSystemSlug.system)?.platform_id; } - - // check if automatic or custom path found existing binary. - // This might not be the actual emulator but I don't care. - const exists = !!execPath && await fs.exists(execPath.path); const systems = Array.from(new Set(system_slug.filter(s => s.system).map(s => s.system!))); - if (exists) + if (execPaths.some(p => p.exists)) { systems.forEach(s => platformViability.set(s, true)); } - return { - emulator: emulator, - path: execPath, - exists: exists, + const storeEmulator = await plugins.hooks.store.fetchEmulator.promise({ id: emulator }); + + if (storeEmulator) + { + storeEmulator.validSources = execPaths; + storeEmulator.integrations = integrations; + return { ...storeEmulator, isCritical: false }; + } + + const em: FrontEndEmulator & { isCritical: boolean; } = { + source: 'local', + name: emulator, + logo: platform ? `/api/romm/platform/local/${platform}/cover` : '', + systems: systems.map(s => platformLookup.get(s)).filter(s => !!s).map(e => ({ iconUrl: `/api/romm/image/romm/assets/platforms/${e.es_slug}.svg`, name: e.platform_name ?? 'Unknown', id: e.es_slug ?? '' })), + gameCount: 0, isCritical: false, - path_cover: platform ? `/api/romm/platform/local/${platform}/cover` : null, - systems: systems.map(s => platformLookup.get(s)).filter(s => !!s) + validSources: execPaths, + integrations }; + + return em; })); finalEmulators.push({ - emulator: 'emulatorjs', - exists: true, - path: { path: 'localhost', type: 'js' }, - path_cover: `/api/romm/image?url=${encodeURIComponent('https://emulatorjs.org/logo/EmulatorJS.png')}`, + source: 'local', + name: 'EMULATORJS', + validSources: [{ binPath: `${SERVER_URL(host)}`, type: 'embedded', exists: true }], + logo: `/api/romm/image?url=${encodeURIComponent('https://emulatorjs.org/logo/EmulatorJS.png')}`, + systems: [], + gameCount: 0, isCritical: false, - systems: [] + description: "Embedded Emulator. Uses Retroarch Cores", + integrations: [] }); return finalEmulators.map(e => { - e.isCritical = !e.systems.filter(s => s?.es_slug).some(s => !!platformViability.get(s?.es_slug!)); + e.isCritical = !e.systems.filter(s => s?.id).some(s => !!platformViability.get(s?.id!)); return e; }); -} - -/** - * Only emulators we strictly need based on local games. Emulator JS is included as bundled. - * If there is even single emulator for a system don't include emulators for that system. - */ -/*export async function getMissingEmulators () -{ - const localGames = await db.query.games.findMany({ - columns: { - platform_id: true, - slug: true - }, - with: { - platform: { - columns: { - name: true, - es_slug: true - } - }, - } - }); - - const platformLookup = new Map(localGames.map(g => [g.platform.es_slug, g])); - const platformViability = new Map(localGames.map(g => [g.platform.es_slug, false])); - - // all commands based on the local games - const commands = await emulatorsDb.query.commands.findMany({ - columns: { command: true }, - where: inArray(emulatorSchema.commands.system, Array.from(new Set(localGames.filter(g => g.platform.es_slug).map(s => s.platform.es_slug!)))), - with: { system: { columns: { name: true } } } - }); - - // get all emulators in said commands - const emulators = commands - .flatMap(command => - { - const matches = command.command.match(/(?<=%EMULATOR_)[\w-]+(?=%)/); - if (!matches) - { - return undefined; - } - - return matches?.map(m => ({ emulator: m, system: command.system?.name })); - } - ).filter(c => !!c); - - const groupedEmulators = Map.groupBy(emulators, ({ emulator }) => emulator); - const finalEmulators = await Promise.all(Array.from(groupedEmulators.entries()).map(async ([emulator, system_slug]) => - { - let execPath: { path: string; type: string, } | undefined; - if (customEmulators.has(emulator)) - { - execPath = { path: customEmulators.get(emulator), type: 'custom' }; - } else - { - execPath = await findExecByName(emulator); - } - - let platform: number | null | undefined = null; - if (system_slug.length <= 1) - { - platform = platformLookup.get(system_slug[0].system)?.platform_id; - } - - // check if automatic or custom path found existing binary. - // This might not be the actual emulator but I don't care. - const exists = !!execPath && await fs.exists(execPath.path); - const systems = Array.from(new Set(system_slug.map(s => s.system))); - if (exists) - { - systems.forEach(s => platformViability.set(s, true)); - } - - return { - emulator: emulator, - path: execPath, - exists: exists, - isCritical: false, - path_cover: platform ? `/api/romm/platform/local/${platform}/cover` : null, - systems: systems.map(s => platformLookup.get(s)).filter(s => !!s) - }; - })); - - return finalEmulators.map(e => - { - e.isCritical = !e.systems.filter(s => s?.es_slug).some(s => !!platformViability.get(s?.es_slug!)); - return e; - }); -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/bun/api/settings/settings.ts b/src/bun/api/settings/settings.ts index dda53b9..ebd5b91 100644 --- a/src/bun/api/settings/settings.ts +++ b/src/bun/api/settings/settings.ts @@ -1,12 +1,17 @@ import z from "zod"; -import { SettingsSchema } from "@shared/constants"; +import { SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared'; import Elysia, { status } from "elysia"; -import { config, customEmulators, taskQueue } from "../app"; +import { config, customEmulators, plugins, taskQueue } from "../app"; import fs from 'node:fs/promises'; import { existsSync } from "node:fs"; import { InstallJob } from "../jobs/install-job"; import { move } from "fs-extra"; import { getRelevantEmulators } from "./services"; +import type { JSONSchema7 } from "json-schema"; +import ReloadPluginsJob from "../jobs/reload-plugins-job"; +import { pluginZodRegistry } from "../plugins/plugin-manager"; +import { TestDownloadJob } from "../jobs/test-download-job"; +import { randomUUIDv7 } from "bun"; export const settings = new Elysia({ prefix: '/api/settings' }) .get('/emulators/automatic', async () => @@ -77,18 +82,63 @@ export const settings = new Elysia({ prefix: '/api/settings' }) drive: z.string().optional() }) }) - .get("/:id", async ({ params: { id } }) => + .get("local/:id", async ({ params: { id } }) => { const value = config.get(id); return { value: value }; }, { params: z.object({ id: z.keyof(SettingsSchema) }), - }).post('/:id', + }).post('local/:id', async ({ params: { id }, body: { value }, }) => { config.set(id, value); }, { params: z.object({ id: z.keyof(SettingsSchema) }), body: z.object({ value: z.any() }), - }); + }) + .get('/definitions/:source', async ({ params: { source } }) => + { + return plugins.plugins[decodeURIComponent(source)].plugin.settingsSchema?.toJSONSchema() as JSONSchema7; + }) + .get('/actions/:source', async ({ params: { source } }) => + { + const plugin = plugins.plugins[decodeURIComponent(source)]?.plugin; + if (!plugin.eventsNames) return []; + return plugin.eventsNames; + }) + .post('/actions/:source/:id', async ({ params: { source, id } }) => + { + return await plugins.plugins[decodeURIComponent(source)]?.plugin.onEvent?.(decodeURIComponent(id)); + }) + .get('/:source/:id', async ({ params: { source, id } }) => + { + return { value: plugins.plugins[decodeURIComponent(source)].config?.get(decodeURIComponent(id)) }; + }) + .post('/test/download', async () => + { + taskQueue.enqueue(randomUUIDv7(), new TestDownloadJob()); + }) + .put('/:source/:id', async ({ params: { source, id }, body: { value } }) => + { + const plugin = plugins.plugins[decodeURIComponent(source)]; + if (!plugin.config) return status("Not Found", "Plugin has no config"); + const settingSchema = plugin.plugin.settingsSchema?.shape[decodeURIComponent(id)] as z.ZodObject; + if (!settingSchema) return status("Not Found", "Could not find setting"); + const meta = pluginZodRegistry.get(settingSchema); + + if (meta?.readOnly) + { + return; + } + + plugin.config?.set(id, value); + + if (meta?.requiresRestart) + { + await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); + } + }, + { + body: z.object({ value: z.any() }) + }); diff --git a/src/bun/api/store/services/emulatorsService.ts b/src/bun/api/store/services/emulatorsService.ts new file mode 100644 index 0000000..6dfcde1 --- /dev/null +++ b/src/bun/api/store/services/emulatorsService.ts @@ -0,0 +1,114 @@ +import { config, plugins } from "../../app"; +import { getOrCached, getOrCachedGithubRelease } from "../../cache"; +import path from "node:path"; +import { EmulatorSourceEntryType, EmulatorSupport, ScoopPackageSchema, EmulatorPackageType, EmulatorDownloadInfoType } from "@simeonradivoev/gameflow-sdk/shared"; + +export function findEmulatorPluginIntegration (name: string, validSources: (EmulatorSourceEntryType | undefined)[]): EmulatorSupport[] +{ + const hasSupport = validSources.concat(undefined).map(s => + { + const support = plugins.hooks.games.emulatorLaunchSupport.call({ emulator: name, source: s }); + if (support) + { + return { ...support, source: s }; + } + + return undefined; + }).filter(s => !!s); + + if (hasSupport.length <= 0) return []; + return hasSupport; +} + +export function getEmulatorPath (emulator: string) +{ + return path.join(config.get('downloadPath'), "emulators", emulator); +} + +export async function getEmulatorDownload (emulator: EmulatorPackageType, source: string) +{ + if (!emulator.downloads) throw new Error("Emulator has no downloads"); + + const validDownloads = emulator.downloads[`${process.platform}:${process.arch}`]; + if (!validDownloads) throw new Error(`Now downloads in ${emulator.name} for platform ${process.platform}:${process.arch}`); + + const validDownload = validDownloads.find(d => d.type === source); + if (!validDownload) throw new Error(`Download type ${source} not found`); + + let downloadUrl: URL; + let versionInfo: EmulatorDownloadInfoType = { + id: "", + downloadDate: new Date(), + type: validDownload.type + }; + if (validDownload.type === 'github') + { + const latestRelease = await getOrCachedGithubRelease(validDownload.path); + const glob = new Bun.Glob(validDownload.pattern); + const validAsset = latestRelease.assets.find(a => glob.match(a.name)); + if (!validAsset) throw new Error("Could Not Find Valid Asset"); + downloadUrl = new URL(validAsset.browser_download_url); + versionInfo.version = latestRelease.tag_name; + versionInfo.url = latestRelease.url; + versionInfo.id = String(latestRelease.id); + versionInfo.description = latestRelease.body; + + } else if (validDownload.type === 'direct') + { + downloadUrl = new URL(validDownload.url); + versionInfo.id = validDownload.url; + versionInfo.url = validDownload.url; + } else if (validDownload.type === 'scoop') + { + const data = await getOrCachedScoopPackage(emulator.name, validDownload.url); + let scoopDownload: URL | undefined; + if (data) + { + if (data.url) + { + scoopDownload = new URL(data.url); + } else if (data.architecture) + { + if (process.arch === 'x64' && data.architecture["64bit"]) + { + scoopDownload = new URL(data.architecture["64bit"].url); + } else if (process.arch === "arm64" && data.architecture["arm64"]) + { + scoopDownload = new URL(data.architecture["arm64"].url); + } + } + } + + if (scoopDownload) + { + downloadUrl = scoopDownload; + versionInfo.version = data?.version; + versionInfo.url = data?.url; + versionInfo.description = data?.description; + } else + { + throw new Error("Could not find scoop download"); + } + } else + { + throw new Error("Download Type Unsupported"); + } + + return { url: downloadUrl, info: versionInfo }; +} + +export async function getOrCachedScoopPackage (id: string, url: string) +{ + const data = await getOrCached(`scoop-dl-${id}`, async () => + { + const res = await fetch(url); + if (res.ok) + { + return ScoopPackageSchema.parseAsync(await res.json()); + } + console.error(res.statusText); + return undefined; + }); + + return data; +} \ No newline at end of file diff --git a/src/bun/api/store/services/gamesService.ts b/src/bun/api/store/services/gamesService.ts index 4221e5a..b475b89 100644 --- a/src/bun/api/store/services/gamesService.ts +++ b/src/bun/api/store/services/gamesService.ts @@ -1,59 +1,55 @@ -import { GithubManifestSchema, StoreGameSchema } from "@/shared/constants"; -import { CACHE_KEYS, getOrCached } from "../../cache"; +import { and, eq, or } from "drizzle-orm"; +import { config, emulatorsDb } from '../../app'; +import path from "node:path"; +import fs from 'node:fs/promises'; +import * as emulatorSchema from '@schema/emulators'; +import { EmulatorSystem, EmulatorPackageType, EmulatorPackageSchema } from "@simeonradivoev/gameflow-sdk/shared"; -export async function getStoreGameManifest () +export function getStoreRootFolder () { - return getOrCached(CACHE_KEYS.STORE_GAME_MANIFEST, async () => - { - const store = await fetch('https://api.github.com/repos/dragoonDorise/EmuDeck/git/trees/50261b66d69c1758efa28c6d7c54e45259a0c9c5?recursive=true').then(r => r.json()).then(data => GithubManifestSchema.parseAsync(data)); - - return store.tree.filter((e: any) => - { - if (e.type === 'blob' && e.path !== "featured.json") - { - return true; - } - return false; - }); - }); + const downlodDir = config.get('downloadPath'); + return path.join(downlodDir, "store"); } -export async function getStoreGames (gamesManifest: any[], filter?: { limit?: number; offset?: number; }) +export function getStoreFolder () { - const offset = filter?.offset ?? 0; - const limit = Math.min(50, filter?.limit ?? 10); + if (process.env.CUSTOM_STORE_PATH) return process.env.CUSTOM_STORE_PATH; + return path.join(getStoreRootFolder(), "node_modules", process.env.STORE_PACKAGE_NAME ?? "@simeonradivoev/gameflow-store"); +} - const games = await Promise.all(gamesManifest.slice(offset, Math.min(offset + limit, gamesManifest.length)).map((e: any) => +export async function getStoreEmulatorPackage (id: string) +{ + const emulatorPath = path.join(getStoreFolder(), "buckets", "emulators", `${id}.json`); + if (await fs.exists(emulatorPath)) + return EmulatorPackageSchema.parseAsync(JSON.parse(await fs.readFile(emulatorPath, 'utf-8'))); + return undefined; +} + +export async function getAllStoreEmulatorPackages () +{ + const emulatorsBucket = path.join(getStoreFolder(), "buckets", "emulators"); + const emulators = await fs.readdir(emulatorsBucket); + const emulatorsRawData = await Promise.all(emulators.map(e => fs.readFile(path.join(emulatorsBucket, e), 'utf-8'))); + + const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.parse(JSON.parse(d))); + + return emulatesParsed; +} + +export async function buildStoreFrontendEmulatorSystems (emulator: EmulatorPackageType): Promise +{ + const systems = await Promise.all(emulator.systems.map(async system => { - return fetch(e.url).then(e => e.json()).then(game => StoreGameSchema.parseAsync(JSON.parse(atob(game.content.replace(/\n/g, ""))))); + const rommSystem = await emulatorsDb.query.systemMappings.findFirst({ + where: or(and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.system, system)), and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.sourceSlug, system))) + }); + + const esSystem = await emulatorsDb.query.systems.findFirst({ where: or(eq(emulatorSchema.emulators.name, system), eq(emulatorSchema.emulators.name, rommSystem?.system ?? '')), columns: { fullname: true } }); + + let icon: string = `/api/romm/image/romm/assets/platforms/${rommSystem?.sourceSlug ?? system}.svg`; + + return { id: system, romm_slug: rommSystem?.sourceSlug ?? undefined, name: esSystem?.fullname ?? system, iconUrl: icon } satisfies EmulatorSystem; })); - return games; -} - -export function extractStoreGameSourceId (id: string) -{ - const gameId = id.split('@'); - if (gameId.length !== 2) - throw new Error("Store ID should include platform and name with @ separator"); - return { system: gameId[0], id: gameId[1] }; -} - -export function getStoreGameFromId (id: string) -{ - const data = extractStoreGameSourceId(id); - return getStoreGame(data.system, data.id); -} - -export async function getStoreGame (system: string, id: string) -{ - return getStoreGameFromPath(`${system}/${encodeURIComponent(id)}.json`); -} - -export async function getStoreGameFromPath (path: string) -{ - const game = await getOrCached(CACHE_KEYS.STORE_GAME(path), () => fetch(`https://cdn.jsdelivr.net/gh/dragoonDorise/EmuDeck/store/${path}`) - .then(e => e.json()) - .then(g => StoreGameSchema.parseAsync(g))); - return game; + return systems; } \ No newline at end of file diff --git a/src/bun/api/store/store.ts b/src/bun/api/store/store.ts index 3720c96..7706699 100644 --- a/src/bun/api/store/store.ts +++ b/src/bun/api/store/store.ts @@ -1,61 +1,28 @@ -import Elysia from "elysia"; -import { config, customEmulators, db } from "../app"; +import Elysia, { status } from "elysia"; +import { config, db, plugins, taskQueue } from "../app"; import path from "node:path"; import fs from 'node:fs/promises'; -import { EmulatorPackageSchema, EmulatorPackageType, FrontEndEmulator, FrontEndEmulatorDetailed, StoreGameSchema } from "@/shared/constants"; -import { findExec } from "../games/services/launchGameService"; -import { emulatorsDb } from '../app'; -import { and, eq } from "drizzle-orm"; -import * as emulatorSchema from '@schema/emulators'; import * as appSchema from '@schema/app'; import z from "zod"; -import { convertLocalToFrontendDetailed, convertStoreToFrontendDetailed, getLocalGameMatch } from "../games/services/utils"; +import { convertLocalToFrontendDetailed, getLocalGameMatch } from "../games/services/utils"; import { getPlatformsApiPlatformsGet } from "@/clients/romm"; import { CACHE_KEYS, getOrCached } from "../cache"; +import { getStoreFolder } from "./services/gamesService"; +import { EmulatorDownloadJob } from "../jobs/emulator-download-job"; +import { BiosDownloadJob } from "../jobs/bios-download-job"; +import { findEmulatorPluginIntegration, getEmulatorPath } from "./services/emulatorsService"; +import { EmulatorSourceEntryType, FrontEndEmulator, FrontEndGameTypeDetailed, PluginBunDetailsSchema, PluginEntrySchema, EmulatorDownloadInfoSchema } from "@simeonradivoev/gameflow-sdk/shared"; +import PQueue from "p-queue"; +import { hasPackage, runBunPackageCommand } from "../plugins/services"; +import { semver } from "bun"; -export function getStoreFolder () -{ - const downlodDir = config.get('downloadPath'); - return path.join(downlodDir, "store"); -} - -async function getAllStoreEmulatorPackages () -{ - const downlodDir = config.get('downloadPath'); - const emulatorsBucket = path.join(downlodDir, "store", "buckets", "emulators"); - const emulators = await fs.readdir(emulatorsBucket); - const emulatorsRawData = await Promise.all(emulators.map(e => fs.readFile(path.join(emulatorsBucket, e), 'utf-8'))); - - const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.safeParse(JSON.parse(d))).filter(e => - { - if (e.error) - { - console.error(e.error); - } - return e.data; - }).map(e => e.data!); - - return emulatesParsed; -} - -async function buildSystems (emulator: EmulatorPackageType) -{ - const systems = await Promise.all(emulator.systems.map(async system => - { - const rommSystem = await emulatorsDb.query.systemMappings.findFirst({ - where: and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.system, system)) - }); - - const esSystem = await emulatorsDb.query.systems.findFirst({ where: eq(emulatorSchema.emulators.name, system), columns: { fullname: true } }); - - let icon: string = `/api/romm/image/romm/assets/platforms/${rommSystem?.sourceSlug ?? system}.svg`; - - return { id: system, name: esSystem?.fullname ?? system, icon: icon }; - })); - - return systems; -} +const npmQueue = new PQueue({ intervalCap: 60, interval: 1000 * 60, strict: true }); +const pluginsResponseSchema = z.object({ + objects: z.array(PluginEntrySchema), + total: z.number(), + time: z.coerce.date() +}); export const store = new Elysia({ prefix: '/api/store' }) .get('/emulators', async ({ query }) => @@ -65,49 +32,41 @@ export const store = new Elysia({ prefix: '/api/store' }) console.error(e); return undefined; }); - const emulatesParsed = await getAllStoreEmulatorPackages(); - let frontEndEmulators = await Promise.all(emulatesParsed - .filter(e => e.os.includes(process.platform as any)) - .map(async (emulator) => - { - let execPath: { path: string; type: string; } | undefined; - const esEmulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, emulator.name) }); - if (esEmulator) + + let frontEndEmulators: FrontEndEmulator[] = []; + await plugins.hooks.store.fetchEmulators.promise({ emulators: frontEndEmulators, search: query.search }); + + await Promise.all(frontEndEmulators.map(async e => + { + const gameCounts = e.systems.map((s) => + { + const romPlatform = rommPlatforms?.find(p => p.slug === (s.romm_slug ?? s.id)); + if (romPlatform) { - if (customEmulators.has(emulator?.name)) - { - execPath = { path: customEmulators.get(emulator.name), type: 'custom' }; - } else - { - execPath = await findExec(esEmulator); - } + return romPlatform.rom_count; } - const exists = !!execPath && await fs.exists(execPath.path); - const systems = await buildSystems(emulator); + return 0; - const gameCounts = await Promise.all(systems.map(async (s) => - { - const rommMapping = await emulatorsDb.query.systemMappings.findFirst({ where: and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.system, s.id)) }); - const romPlatform = rommPlatforms?.find(p => p.slug === (rommMapping?.sourceSlug ?? s.id)); - if (romPlatform) - { - return romPlatform.rom_count; - } + }); - return 0; + const execPaths: EmulatorSourceEntryType[] = []; + await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: e.name, sources: execPaths }); + const integrations = findEmulatorPluginIntegration(e.name, execPaths); - })); - - const gameCount = gameCounts.reduce((a, c) => a + c); - - return { ...emulator, exists, systems, gameCount } satisfies FrontEndEmulator; - })); + e.gameCount = gameCounts.reduce((a, c) => a + c); + e.integrations = integrations; + })); if (query.missing) { - frontEndEmulators = frontEndEmulators.filter(e => !e.exists); + frontEndEmulators = frontEndEmulators.filter(e => + { + if (e.validSources.some(s => s.exists)) return false; + if (query.related && e.name === query.related) return false; + return true; + }); } if (query.orderBy === 'importance') @@ -131,71 +90,151 @@ export const store = new Elysia({ prefix: '/api/store' }) query: z.object({ limit: z.coerce.number().optional(), missing: z.stringbool().optional().describe("Show Only Non Installed emulators"), - orderBy: z.enum(['name', 'recently_updated', 'importance']).optional() + orderBy: z.enum(['name', 'recently_updated', 'importance']).optional(), + related: z.string().optional(), + search: z.string().optional() }) }) .get('/games/featured', async () => { - const response = await fetch('https://cdn.jsdelivr.net/gh/dragoonDorise/EmuDeck/store/featured.json'); - const games = await z.object({ featured: z.array(StoreGameSchema) }).parseAsync(await response.json()); - return Promise.all(games.featured.map(async g => + const games: FrontEndGameTypeDetailed[] = []; + await plugins.hooks.store.fetchFeaturedGames.promise({ games }); + + return Promise.all(games.map(async g => { - const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(`${g.system}@${g.title}`, 'store') }); + const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(g.id.id, g.id.source) }); if (localGame) return convertLocalToFrontendDetailed(localGame); - return convertStoreToFrontendDetailed(g.system, g.title, g); + return g; })); }) .get('/stats', async () => { - const emulatesParsed = await getAllStoreEmulatorPackages(); - const storeEmulatorCount = emulatesParsed.filter(e => e.os.includes(process.platform as any)).length; + let frontEndEmulators: FrontEndEmulator[] = []; + await plugins.hooks.store.fetchEmulators.promise({ emulators: frontEndEmulators }); + const storeEmulatorCount = frontEndEmulators.length; const gameCount = await db.$count(appSchema.games); return { storeEmulatorCount, gameCount }; }) + .get('/plugin', async ({ query: { plugin } }) => + { + const pluginsRes = await runBunPackageCommand(['info', plugin]); + const pluginData = await PluginBunDetailsSchema.parseAsync(JSON.parse(pluginsRes)); + const existingVersion = plugins.plugins[plugin]?.description.version; + + return { + ...pluginData, + installed: !!plugins.plugins[plugin] || await hasPackage(plugin), + update: existingVersion && semver.order(pluginData.version, existingVersion) > 0 ? { from: existingVersion } : undefined + }; + }, + { + query: z.object({ plugin: z.string() }) + }) + .get('/plugins', async ({ query: { search } }) => + { + //TODO: Find a better way to search keywords and a search term at the same time + const pluginsRes = await npmQueue.add(() => fetch(`https://registry.npmjs.com/-/v1/search?text=keywords:gameflow-plugin`)); + if (!pluginsRes.ok) return status(pluginsRes.status, pluginsRes.statusText); + const data: z.infer = await pluginsRes.json(); + if (search) + { + data.objects = data.objects.filter(o => + { + if (o.package.description && o.package.description.includes(search)) return true; + if (o.package.name.includes(search)) return true; + if (o.package.keywords.includes(search)) return true; + return false; + }); + data.total = data.objects.length; + } + await Promise.all(data.objects.map(async o => + { + const existingVersion = plugins.plugins[o.package.name]?.description.version; + o.installed = !!plugins.plugins[o.package.name] || await hasPackage(o.package.name); + o.update = existingVersion && semver.order(o.package.version, existingVersion) > 0 ? { from: existingVersion } : undefined; + })); + return data as any; + }, { + query: z.object({ search: z.string().optional() }), + response: pluginsResponseSchema + }) + .get('/media/*', async ({ params }) => + { + return Bun.file(path.join(getStoreFolder(), params["*"])); + }) .get('/screenshot/emulator/:id/:name', async ({ params: { id, name } }) => { - const downlodDir = config.get('downloadPath'); - return Bun.file(path.join(downlodDir, "store", "media", "screenshots", id, name)); + return Bun.file(path.join(getStoreFolder(), "media", "screenshots", id, name)); }, { params: z.object({ id: z.string(), name: z.string() }) }) - .get('/details/emulator/:id', async ({ params: { id } }) => + .get('/emulator/:id/update', async ({ params: { id } }) => { - const downlodDir = config.get('downloadPath'); - const emulatorPath = path.join(downlodDir, "store", "buckets", "emulators", `${id}.json`); - const emulatorScreenshotsPath = path.join(downlodDir, "store", "media", "screenshots", id); - const emulatorPackage = await EmulatorPackageSchema.parseAsync(JSON.parse(await fs.readFile(emulatorPath, 'utf-8'))); - - const systems = await buildSystems(emulatorPackage); - let execPath: { path: string; type: string; } | undefined; - const esEmulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, emulatorPackage.name) }); - - if (esEmulator) + return plugins.hooks.store.fetchDownload.promise({ id }); + }, { - if (customEmulators.has(emulatorPackage?.name)) - { - execPath = { path: customEmulators.get(emulatorPackage.name), type: 'custom' }; - } else - { - execPath = await findExec(esEmulator); - } + response: z.union([z.intersection(EmulatorDownloadInfoSchema, z.object({ hasUpdate: z.boolean() })), z.undefined()]) + }) + .get('/emulator/:id', async ({ params: { id } }) => + { + const emulator = await plugins.hooks.store.fetchEmulator.promise({ id }); + if (!emulator) return status("Not Found"); + const sources: EmulatorSourceEntryType[] = []; + await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: emulator.name, sources }); + const integrations = findEmulatorPluginIntegration(emulator.name, sources); + emulator.validSources = sources; + emulator.integrations = integrations; + return emulator; + }, { params: z.object({ id: z.string() }) }) + .post('/install/emulator/:id/:source', async ({ params: { source, id }, body }) => + { + if (taskQueue.hasActiveOfType(EmulatorDownloadJob)) + { + return status("Conflict", "Installation already running"); + } + const job = new EmulatorDownloadJob(id, source, body); + return taskQueue.enqueue(EmulatorDownloadJob.id, job); + }, { + body: z.object({ isUpdate: z.boolean().optional() }).optional() + }) + .delete('/emulator/:id', async ({ params: { id } }) => + { + const storeEmulatorFolder = getEmulatorPath(id); + const existingPackagePath = `${storeEmulatorFolder}.json`; + let hadDelete = false; + if (await fs.exists(existingPackagePath)) + { + await fs.rm(existingPackagePath); + hadDelete = true; } + if (await fs.exists(storeEmulatorFolder)) + { + fs.rm(storeEmulatorFolder, { recursive: true }); + hadDelete = true; + } - const screenshots = await fs.exists(emulatorScreenshotsPath) ? await fs.readdir(emulatorScreenshotsPath) : []; - const exists = !!execPath && await fs.exists(execPath.path); - const emulator: FrontEndEmulatorDetailed = { - ...emulatorPackage, - systems, - exists, - status: { - source: execPath?.type, - location: execPath?.path - }, - screenshots: screenshots.map(s => `/api/store/screenshot/emulator/${id}/${s}`) - }; + return hadDelete ? status("OK") : status("Not Found"); + }) + .post('/download/bios/:id', async ({ params: { id } }) => + { + if (taskQueue.findJob(BiosDownloadJob.query({ id }), BiosDownloadJob)) + { + return status("Conflict", "Bios Download Already Active"); + } - return emulator; - }, { params: z.object({ id: z.string() }) }); \ No newline at end of file + return taskQueue.enqueue(BiosDownloadJob.query({ id }), new BiosDownloadJob(id)); + }) + .delete('/bios/:id', async ({ params: { id } }) => + { + const biosFolder = path.join(config.get('downloadPath'), "bios", id); + if (await fs.exists(biosFolder)) + { + await fs.rm(biosFolder, { recursive: true }); + } else + { + return status("Not Found"); + } + }); \ No newline at end of file diff --git a/src/bun/api/system.ts b/src/bun/api/system.ts index 31741a4..2124144 100644 --- a/src/bun/api/system.ts +++ b/src/bun/api/system.ts @@ -2,16 +2,31 @@ import Elysia from "elysia"; import open from 'open'; import z from "zod"; import os from 'node:os'; -import { cachePath, config, events } from "./app"; -import { isSteamDeck, openExternal } from "../utils"; +import { cachePath, config, events, taskQueue } from "./app"; +import { getAppVersion, isSteamDeck, openExternal } from "../utils"; import fs from 'node:fs/promises'; import buildNotificationsStream from "./notifications"; import path, { dirname } from "node:path"; -import { DirSchema, DownloadsDrive } from "@/shared/constants"; +import { SystemInfoSchema, DirSchema, DownloadsDrive } from '@simeonradivoev/gameflow-sdk/shared'; import { getDevices, getDevicesCurated } from "./drives"; import getFolderSize from "get-folder-size"; import si from 'systeminformation'; -import { getStoreFolder } from "./store/store"; +import { getStoreFolder } from "./store/services/gamesService"; +import ReloadPluginsJob from "./jobs/reload-plugins-job"; +import { semver } from "bun"; +import { getOrCachedGithubRelease } from "./cache"; +import SelfUpdateJob from "./jobs/self-update-job"; + +async function checkUpdate (force?: boolean) +{ + const latest = await getOrCachedGithubRelease('simeonradivoev/gameflow-deck', force); + if (!latest || !latest.tag_name) return { + hasUpdate: 0, + version: getAppVersion() + }; + const hasUpdate = semver.order(latest.tag_name, getAppVersion()); + return { hasUpdate, version: latest.tag_name, info: latest.body }; +} export const system = new Elysia({ prefix: '/api/system' }) .post('/show_keyboard', async ({ body: { XPosition, YPosition, Width, Height } }) => @@ -51,7 +66,8 @@ export const system = new Elysia({ prefix: '/api/system' }) machine: os.machine(), source, cacheSize: (await fs.stat(cachePath)).size, - storeSize: (await getFolderSize(getStoreFolder())).size + storeSize: (await getFolderSize(getStoreFolder())).size, + version: getAppVersion() }; }) .get('/notifications', ({ set }) => @@ -61,17 +77,99 @@ export const system = new Elysia({ prefix: '/api/system' }) set.headers['connection'] = 'keep-alive'; return new Response(buildNotificationsStream()); }) - .get('/info/battery', async () => + .get('/notifications/all', ({ }) => { - return si.battery(); + }) - .get('/info/wifi', async () => - { - return si.wifiConnections(); - }) - .get('/info/bluetooth', async () => - { - return si.bluetoothDevices(); + .ws('/info/system', { + response: z.discriminatedUnion('type', [ + z.object({ type: z.literal('info'), data: SystemInfoSchema }), + z.object({ type: z.literal('focus') }), + z.object({ type: z.literal('loading'), progress: z.number(), state: z.string().optional() }), + z.object({ type: z.literal('activeTask'), progress: z.number().nullable() }), + z.object({ type: z.literal('loaded') }), + ]), + async open (ws) + { + const existingLoading = taskQueue.findJob(ReloadPluginsJob.id, ReloadPluginsJob); + if (existingLoading) ws.send({ type: 'loading', progress: existingLoading.progress, state: existingLoading.state }); + else ws.send({ type: 'loaded' }); + + ws.send({ type: 'activeTask', progress: taskQueue.getActiveJobs()[0]?.progress }); + + const startInfo = async () => + { + const battery = await si.battery(); + const wifi = await si.wifiConnections(); + const bluetooth = await si.bluetoothDevices(); + ws.send({ + type: 'info', + data: { + battery: battery, + wifiConnections: wifi, + bluetoothDevices: bluetooth + } + }, true); + }; + startInfo(); + + const handleFocus = () => ws.send({ type: 'focus' }); + events.on('focus', handleFocus); + const dispose: (() => void)[] = []; + + dispose.push(taskQueue.on('progress', e => + { + ws.send({ type: 'activeTask', progress: e.progress }); + + if (e.id === ReloadPluginsJob.id) + { + ws.send({ type: "loading", progress: e.progress, state: e.state }); + } + else if (e.id === SelfUpdateJob.id) + { + ws.send({ type: "loading", progress: e.progress, state: e.state }); + } + })); + dispose.push(taskQueue.on('started', e => + { + ws.send({ type: 'activeTask', progress: 0 }); + + if (e.id === ReloadPluginsJob.id) + ws.send({ type: "loading", progress: e.job.progress, state: e.job.state }); + else if (e.id === SelfUpdateJob.id) + ws.send({ type: "loading", progress: e.job.progress, state: e.job.state }); + })); + dispose.push(taskQueue.on('ended', e => + { + ws.send({ type: 'activeTask', progress: null }); + if (e.id !== ReloadPluginsJob.id && e.id !== SelfUpdateJob.id) return; + ws.send({ type: "loaded" }); + })); + + (ws.data as any).dispose = [...dispose, () => + { + events.removeListener('focus', handleFocus); + }]; + (ws.data as any).observer = setInterval(async () => + { + const battery = await si.battery(); + const wifi = await si.wifiConnections(); + const bluetooth = await si.bluetoothDevices(); + ws.send({ + type: 'info', + data: { + battery: battery, + wifiConnections: wifi, + bluetoothDevices: bluetooth + } + }, true); + }, 1000 * 30); + }, + close (ws) + { + clearInterval((ws.data as any).observer); + (ws.data as any).dispose.forEach((dispose: any) => dispose()); + } }) .get('/drives', async () => { @@ -149,6 +247,10 @@ export const system = new Elysia({ prefix: '/api/system' }) { currentPath = path.resolve(process.cwd(), currentPath); } + const currentPathExists = await fs.exists(currentPath); + if (!currentPathExists) currentPath = dirname(process.cwd()); + const currentPathStat = await fs.stat(currentPath); + if (!currentPathStat.isDirectory()) currentPath = dirname(currentPath); const paths = await fs.readdir(currentPath, { withFileTypes: true }); return { name: path.basename(currentPath), @@ -178,4 +280,16 @@ export const system = new Elysia({ prefix: '/api/system' }) await openExternal(url); }, { body: z.object({ url: z.string() }) + }) + .get('/update', async () => + { + return checkUpdate(); + }) + .post('/update', async () => + { + return taskQueue.enqueue(SelfUpdateJob.id, new SelfUpdateJob()); + }) + .post('/update/check', async () => + { + return checkUpdate(true); }); \ No newline at end of file diff --git a/src/bun/api/task-queue.ts b/src/bun/api/task-queue.ts deleted file mode 100644 index 002f326..0000000 --- a/src/bun/api/task-queue.ts +++ /dev/null @@ -1,231 +0,0 @@ - -import EventEmitter from 'node:events'; - -export class TaskQueue -{ - private activeQueue: { context: JobContext, promise?: Promise; }[] = []; - private queue?: { context: JobContext, promise?: Promise; }[] = []; - private events?: EventEmitter = new EventEmitter(); - - public enqueue (id: string, job: IJob): Promise - { - this.disposeSafeguard(); - if (!this.queue || !this.events) throw new Error("Queue disposed"); - const context = new JobContext(id, this.events, job); - this.queue.push({ context }); - return this.processQueue(); - } - - private processQueue (): Promise - { - if (!this.queue) return Promise.resolve(); - const top = this.queue.pop(); - if (top) - { - const promise = top.context.start(); - top.promise = promise; - const index = this.queue.length; - this.activeQueue.push(top); - promise.finally(() => - { - this.activeQueue.splice(index, 1); - setTimeout(this.processQueue); - }); - return promise; - - } - return Promise.resolve(); - } - - private disposeSafeguard () - { - if (!this.queue) throw new Error("Queue disposed"); - } - - public hasActive () - { - return this.activeQueue.length > 0; - } - - public hasActiveOfType (type: any) - { - for (const entry of this.activeQueue) - { - if (entry.context.job instanceof type) - { - return true; - } - } - return false; - } - - public waitForJob (id: string): Promise - { - const job = this.queue?.find(j => j.context.id === id) ?? this.activeQueue?.find(j => j.context.id === id); - return job?.promise ?? Promise.resolve(); - } - - public findJob (id: string): IPublicJob | undefined - { - const job = this.queue?.find(j => j.context.id === id) ?? this.activeQueue?.find(j => j.context.id === id); - return job?.context; - } - - public on (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never): () => void - { - this.events?.on(event, listener); - return () => this.events?.removeListener(event, listener); - } - - public once (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never) - { - this.events?.once(event, listener); - } - - public async close () - { - this.queue = []; - this.activeQueue.forEach(c => c.context.abort()); - return Promise.all(this.activeQueue.map(c => c.promise)); - } -} - -export interface EventsList -{ - started: [e: BaseEvent]; - progress: [e: ProgressEvent]; - abort: [e: AbortEvent]; - /** Called when the job successfully completes */ - completed: [e: CompletedEvent]; - error: [e: ErrorEvent]; - ended: [e: BaseEvent]; -} - -interface BaseEvent -{ - id: string; - job: IPublicJob; -} - -interface ErrorEvent extends BaseEvent -{ - error: unknown; -} - -interface AbortEvent extends BaseEvent -{ - reason?: any; -} - -interface ProgressEvent extends BaseEvent -{ - progress: number; - state?: string; -} - -interface CompletedEvent extends BaseEvent -{ - -} - -export interface IJob -{ - start (context: JobContext): Promise; - exposeData?(): any; -} - -export type JobStatus = 'completed' | 'error' | 'running' | 'waiting' | 'aborted'; - -export interface IPublicJob -{ - progress: number; - state?: string; - status: JobStatus; - job: IJob; - abort: (reason?: any) => void; -} - -export class JobContext implements IPublicJob -{ - private m_id: string; - private m_progress: number = 0; - private m_state?: string; - private running: boolean = false; - private aborted: boolean = false; - private completed: boolean = false; - private error?: any; - private events: EventEmitter; - private abortController: AbortController; - private readonly m_job: IJob; - - constructor(id: string, events: EventEmitter, job: IJob) - { - this.m_id = id; - this.m_job = job; - this.abortController = new AbortController(); - this.abortController.signal.addEventListener('abort', () => - { - this.aborted = true; - this.events.emit('abort', { id: this.m_id, reason: this.abortController.signal.reason, job: this } satisfies AbortEvent); - }); - this.events = events; - } - - public async start (): Promise - { - try - { - this.events.emit('started', { id: this.m_id, job: this }); - await this.m_job.start(this); - this.completed = true; - this.events.emit('completed', { id: this.m_id, job: this }); - - } catch (error) - { - if (error !== 'cancel') - { - console.error(error); - } - - this.events.emit('error', { id: this.m_id, job: this, error }); - this.error = error; - } finally - { - this.running = false; - this.events.emit('ended', { id: this.m_id, job: this }); - } - } - - public get status (): JobStatus - { - if (this.completed) return 'completed'; - if (this.error) return 'error'; - if (this.aborted) return 'aborted'; - if (this.running) return 'running'; - return 'waiting'; - } - - public get id () { return this.m_id; } - - public get job () { return this.m_job; } - - public get abortSignal () { return this.abortController.signal; } - - public get progress () { return this.m_progress; } - - public get state () { return this.m_state; } - - public setProgress (progress: number, state?: string) - { - this.m_progress = progress; - if (state) - this.m_state = state; - this.events.emit('progress', { id: this.m_id, progress, state: state ?? this.m_state, job: this }); - } - - public abort (reason?: any) - { - this.error = reason; - this.abortController.abort(reason); - } -} \ No newline at end of file diff --git a/src/bun/browser.ts b/src/bun/browser.ts index 14d3f27..8d71427 100644 --- a/src/bun/browser.ts +++ b/src/bun/browser.ts @@ -2,26 +2,126 @@ import { killBrowser, spawnBrowser } from './utils/browser-spawner'; import { BrowserParams, BuildParams } from './utils/browser-params'; import os from 'node:os'; import { EventEmitter } from 'node:stream'; +import { dlopen, FFIType, Pointer } from "bun:ffi"; +import { SERVER_URL } from '@/shared/constants'; +import { host } from './utils/host'; +import fs from 'node:fs/promises'; +import { ensureDir } from 'fs-extra'; +import path from 'node:path'; -export default async function init (events: EventEmitter, forceBrowser: boolean, params: BrowserParams) +export default async function init (events: EventEmitter, params: BrowserParams) { - if (forceBrowser) + if (params.forceNWJS) + { + await runNW(events, params); + return; + } + + if (params.forceBrowser) { await runBrowser(events, params); - } else + return; + } + + try { - try + await runWebview(events, params); + return; + } catch (error) + { + console.error(error); + } + + try + { + await runNW(events, params); + return; + } catch (error) + { + console.error(error); + } + + await runBrowser(events, params); +} + +function focusWindow (id: Pointer) +{ + if (process.platform === 'win32') + { + const user32 = dlopen("user32.dll", { + SetForegroundWindow: { args: [FFIType.ptr], returns: FFIType.bool }, + ShowWindow: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.bool }, + BringWindowToTop: { args: [FFIType.ptr], returns: FFIType.bool }, + keybd_event: { args: [FFIType.u8, FFIType.u8, FFIType.u32, FFIType.ptr], returns: FFIType.void }, + }); + + const SW_RESTORE = 9; + + if (id) { - await runWebview(events, params); - } catch (error) - { - await runBrowser(events, params); + user32.symbols.ShowWindow(id, SW_RESTORE); + user32.symbols.keybd_event(0, 0, 0, null); // fake input event + user32.symbols.BringWindowToTop(id); + user32.symbols.SetForegroundWindow(id); } } } +async function runNW (events: EventEmitter, params: BrowserParams) +{ + let nwPath = process.platform === 'win32' ? './bin/nw/nw.exe' : './bin/nw/nw'; + if (process.env.FLATPAK_BUILD) + { + nwPath = '/app/bin/nw/nw'; + } else if (process.env.APPIMAGE) + { + nwPath = path.join(process.env.APPDIR ?? '', 'usr', 'bin', 'nw'); + } + + if (!await fs.exists(nwPath)) + { + throw new Error(`Could not find NW.js at ${nwPath}`); + } + const signalHandler = new AbortController(); + const chromeArgs: string[] = ['--in-process-gpu']; + if (params.isSteamDeckGameMode) + { + chromeArgs.push('--kiosk'); + chromeArgs.push(`--window-size=1280,800`); + } else if (params.windowSize) + { + chromeArgs.push(`--window-size=${params.windowSize.width},${params.windowSize.height}`); + } + if (params.windowPosition) chromeArgs.push(`--window-position=${params.windowPosition.x},${params.windowPosition.y}`); + events.on('exitapp', () => signalHandler.abort()); + const configPath = path.join(params.configPath, 'nw-user-data'); + await ensureDir(configPath); + console.log("NW config path at:", configPath); + const args = [nwPath, `--url=${SERVER_URL(host)}`, `--user-data-dir=${configPath}`]; + + if (process.env.NODE_ENV !== 'development') + { + console.log("Disabling devtools"); + args.push("--disable-devtools"); + } + console.log("Launching NW.js"); + const nwProcess = Bun.spawn(args, { + signal: signalHandler.signal, + killSignal: "SIGKILL", + env: { + ...process.env, + NW_PRE_ARGS: chromeArgs.join(" ") + } + }); + await nwProcess.exited; +} + async function runWebview (events: EventEmitter, params: BrowserParams) { + if (process.platform !== 'win32') + { + throw new Error("Webview only supported on windows"); + } const webviewPath = process.env.IS_BINARY ? `./webview/${os.platform()}` : new URL(`./webview/${os.platform()}`, import.meta.url).href; console.log("Launching Webview Worker at: ", webviewPath); const config: Record = {}; @@ -31,8 +131,6 @@ async function runWebview (events: EventEmitter, params: BrowserParams) config.WINDOW_HEIGHT = String(params.windowSize?.height); } const webviewWorker = new Worker(webviewPath, { - smol: true, - ref: false, env: { ...config, ...process.env as any @@ -41,9 +139,21 @@ async function runWebview (events: EventEmitter, params: BrowserParams) return new Promise((resolve, reject) => { + const handleExit = () => + { + resolve(true); + console.log("Terminating Webview Worker"); + webviewWorker.terminate(); + }; + + let pointer: any = undefined; + webviewWorker.addEventListener('error', e => { console.error(e.message); + events.removeListener('exitapp', handleExit); + // error doesn't termiate the worker, make sure it's unalived + webviewWorker.terminate(); reject(e.error); }); @@ -53,14 +163,16 @@ async function runWebview (events: EventEmitter, params: BrowserParams) { console.log("Webview Destroyed"); resolve(true); + } else if (e.data.type === 'pointer') + { + pointer = e.data.data; } }); - events.on('exitapp', () => + events.on('exitapp', handleExit); + events.on('focus', () => { - resolve(true); - console.log("Terminating Webview Worker"); - webviewWorker.terminate(); + focusWindow(pointer); }); }); } @@ -70,8 +182,7 @@ async function runBrowser (events: EventEmitter, params: BrowserParams) const browserParams = await BuildParams(params); if (!browserParams) { - console.error("Could not find valid browser"); - return Promise.resolve(); + throw new Error("Could not find valid browser"); } else if (!Bun.env.HEADLESS) { @@ -94,6 +205,7 @@ async function runBrowser (events: EventEmitter, params: BrowserParams) { events.on('exitapp', () => { + console.log("Killing Browser"); killBrowser(browser); resolve(true); }); diff --git a/src/bun/index.ts b/src/bun/index.ts index 9ea71be..7ad5803 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -1,30 +1,32 @@ -import { RunBunServer } from './server'; -import { RunAPIServer } from './api/rpc'; -import { cleanup as appCleanup, config, events } from './api/app'; + +import * as app from './api/app'; import init from './browser'; import { dirname } from 'pathe'; import { createInterface } from 'readline'; +import { isSteamDeckGameMode } from './utils'; -const api = RunAPIServer(); -let bunServer: { stop: () => void; } | undefined; - -if (!Bun.env.PUBLIC_ACCESS) +async function cleanup (code: number) { - bunServer = RunBunServer(); + app.cleanup() + .then(() => + { + process.exit(code); + }) + .catch(e => console.error); } -async function cleanup () +await app.load(); + +async function shutdown (code: number) { - console.log("Cleaning Up"); - await appCleanup(); - bunServer?.stop(); - await api.apiServer.stop(true); - await api.cleanup(); - console.log("Finished Cleaning Up"); - process.exit(0); + console.log("Graceful Shutdown"); + await cleanup(code); } -if (Bun.env.HEADLESS) +process.on("SIGINT", () => shutdown(0)); +process.on("SIGTERM", () => shutdown(0)); + +if (process.env.HEADLESS) { const rl = createInterface({ input: process.stdin }); @@ -33,26 +35,31 @@ if (Bun.env.HEADLESS) if (line.trim() === "shutdown") { console.log("Graceful Shutdown"); - await cleanup(); + await cleanup(0); } }); - // Called by user - events.on('exitapp', () => + // Using stdout for communication as ipc doesn't seem to work with dev.ts script + app.events.on('exitapp', () => { - process.send?.({ type: 'exitapp' }); - cleanup(); + process.stdout.write('exitapp\n'); + process.send?.("exitapp"); + cleanup(0); + }); + app.events.on('focus', () => + { + process.stdout.write("focus\n"); + process.send?.("focus"); }); } else { - await init(events, Bun.env.FORCE_BROWSER === "true", { - configPath: dirname(config.path), - windowPosition: config.get('windowPosition'), - windowSize: config.get('windowSize') + await init(app.events, { + configPath: dirname(app.config.path), + windowPosition: app.config.get('windowPosition'), + windowSize: app.config.get('windowSize'), + isSteamDeckGameMode: isSteamDeckGameMode(), + forceBrowser: process.env.FORCE_BROWSER === "true", + forceNWJS: process.env.FORCE_NWJS === "true" }); - await cleanup(); -} - - - - + await cleanup(0); +} \ No newline at end of file diff --git a/src/bun/server.ts b/src/bun/server.ts index 58fd64a..b82971b 100644 --- a/src/bun/server.ts +++ b/src/bun/server.ts @@ -1,62 +1,44 @@ import { SERVER_PORT } from "@shared/constants"; -import path from 'node:path'; -import appInfo from '~/package.json'; import { host } from "./utils/host"; import { appPath } from "./utils"; -import Elysia, { file } from "elysia"; +import Elysia from "elysia"; import cors from "@elysiajs/cors"; -import staticPlugin from "@elysiajs/static"; -export function RunBunServer () +export async function RunBunServer () { console.log("Launching Server on port ", SERVER_PORT); - return new Elysia() + const server = new Elysia() .use(cors()) - .get("/", ({ set }) => - { - set.headers['cross-origin-opener-policy'] = 'same-origin'; - set.headers['cross-origin-embedder-policy'] = 'require-corp'; - return file("./dist/index.html"); + .headers({ + 'cross-origin-embedder-policy': 'credentialless', + 'cross-origin-opener-policy': 'same-origin', + 'cross-origin-resource-policy': 'cross-origin' }) - .get('/emulatorjs', ({ set }) => + .get("/", () => { - set.headers['cross-origin-opener-policy'] = 'same-origin'; - set.headers['cross-origin-embedder-policy'] = 'require-corp'; - set.headers['cross-origin-resource-policy'] = 'cross-origin'; - return file('./dist/emulatorjs/index.html'); + return Bun.file(appPath("./dist/index.html")); }) - .use(staticPlugin({ - indexHTML: false, - assets: "dist", - prefix: "/", - alwaysStatic: true - })).listen({ port: SERVER_PORT, hostname: host }, console.log); - /*return Bun.serve({ - port: SERVER_PORT, - hostname: host, - routes: { - "/": Bun.file(appPath("./dist/index.html")), - // Serve a file by lazily loading it into memory - "/favicon.ico": Bun.file(appPath("./dist/favicon.ico")), - "/emulatorjs/": Bun.file(appPath("./dist/emulatorjs/index.html")), - "/.well-known/appspecific/com.chrome.devtools.json": new Response( - JSON.stringify({ - name: appInfo.name, - version: appInfo.version, - debuggable: true, - }), - { - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-cache", - }, - } - ) - }, - fetch: async (req) => + .get('/emulatorjs', () => { - const url = new URL(req.url); - return new Response(Bun.file(appPath(`./${path.join('dist', url.pathname)}`))); - }, - });*/ + return Bun.file(appPath('./dist/emulatorjs/index.html')); + }) + .get("/*", ({ params }) => Bun.file(appPath(`./dist/${params["*"]}`))); + + + await new Promise((resolve) => + { + server.listen({ port: SERVER_PORT, hostname: host, development: true }, async ({ hostname, port }) => + { + resolve(server); + }); + }); + + await server.modules; + + return { + cleanup: async () => + { + await server.stop(true); + } + }; } \ No newline at end of file diff --git a/src/bun/types/helpers.d.ts b/src/bun/types/helpers.d.ts new file mode 100644 index 0000000..afd8ea5 --- /dev/null +++ b/src/bun/types/helpers.d.ts @@ -0,0 +1,19 @@ +declare module '*.bat' { + const content: string; + export default content; +} + +declare module '*.sh' { + const content: string; + export default content; +} + +declare module '*.ini' { + const content: string; + export default content; +} + +declare module '*.bin' { + const content: string; + export default content; +} \ No newline at end of file diff --git a/src/bun/types/types.d.ts b/src/bun/types/types.d.ts deleted file mode 100644 index dd95180..0000000 --- a/src/bun/types/types.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ChildProcess } from "node:child_process"; - -declare const IS_BINARY: string; - -export type ActiveGame = { - process?: ChildProcess; - gameId: number; - name: string; - command: string; -}; - -interface ObjectConstructor -{ - /** - * Groups members of an iterable according to the return value of the passed callback. - * @param items An iterable. - * @param keySelector A callback which will be invoked for each item in items. - */ - groupBy ( - items: Iterable, - keySelector: (item: T, index: number) => K, - ): Partial>; -} - -interface MapConstructor -{ - /** - * Groups members of an iterable according to the return value of the passed callback. - * @param items An iterable. - * @param keySelector A callback which will be invoked for each item in items. - */ - groupBy ( - items: Iterable, - keySelector: (item: T, index: number) => K, - ): Map; -} \ No newline at end of file diff --git a/src/bun/utils.ts b/src/bun/utils.ts index 33a58cf..6fbc630 100644 --- a/src/bun/utils.ts +++ b/src/bun/utils.ts @@ -1,5 +1,11 @@ -import { $ } from 'bun'; +import { $, sleep } from 'bun'; import path from 'node:path'; +import { SettingsType, KeysWithValueAssignableTo } from '@simeonradivoev/gameflow-sdk/shared'; +import { config } from './api/app'; +import fs from 'node:fs/promises'; +import packageDef from '~/package.json'; + +const archiveRegex = /.(zip|rar|7zip|7z|tar|tar.gz)$/i; export function checkRunning (pid: number) { @@ -68,4 +74,130 @@ export async function openExternal (target: string) { return $`open ${target}`.throws(true); } +} + +export async function hashFile (path: string, algorithm: Bun.SupportedCryptoAlgorithms) +{ + const hasher = new Bun.CryptoHasher(algorithm); + const stream = Bun.file(path).stream(); + const reader = stream.getReader(); + + while (true) + { + const { done, value } = await reader.read(); + if (done) break; + hasher.update(value); + } + + return hasher.digest('hex'); +} + +export class SeededRandom +{ + seed: number; + + constructor(seed?: number) + { + this.seed = seed ?? new Date().getTime(); + } + + next () + { + var x = Math.sin(this.seed++) * 10000; + return x - Math.floor(x); + } +} + +export function shuffleInPlace (array: any[], startSeed?: number) +{ + const random = new SeededRandom(startSeed); + + for (let i = array.length - 1; i > 0; i--) + { + const j = Math.floor(random.next() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} + +export function toggleElementInConfig (id: KeysWithValueAssignableTo>, element: T, enabled: boolean) +{ + const disabled = config.get(id as any) as T[]; + if (enabled) + { + const index = disabled.indexOf(element); + if (index >= 0) + { + config.set('disabledPlugins', disabled.toSpliced(index, 1)); + } + } else + { + const index = disabled.indexOf(element); + if (index < 0) + { + config.set('disabledPlugins', disabled.concat(element)); + } + + } +} + +export async function simulateProgress (setProgress: (p: number) => void, signal?: AbortSignal) +{ + for (let i = 0; i < 10; i++) + { + setProgress(i * 10); + if (signal && signal.aborted) return; + await sleep(1000); + } +} + +export async function moveAllFiles (srcDir: string, destDir: string) +{ + await fs.mkdir(destDir, { recursive: true }); + + const entries = await fs.readdir(srcDir); + for (const entry of entries) + { + const srcPath = path.join(srcDir, entry); + const destPath = path.join(destDir, entry); + + const stats = await fs.stat(srcPath); + if (stats.isDirectory()) + { + await moveAllFiles(srcPath, destPath); + await fs.rmdir(srcPath); // remove empty directory + } else + { + await fs.rename(srcPath, destPath).catch(async () => + { + // fallback to copy+delete if rename fails + await fs.copyFile(srcPath, destPath); + await fs.unlink(srcPath); + }); + } + } +} + +export function getAppVersion () +{ + return process.env.VERSION_OVERRIDE ?? packageDef.version; +} + +export function isArchive (path: string) +{ + return archiveRegex.test(path); +} + +export function IsPluginAllowed (id: string) +{ + if (process.env.PLUGIN_WHITELIST && !process.env.PLUGIN_WHITELIST.includes(id)) + { + return false; + } + + if (process.env.PLUGIN_BLACKLIST && process.env.PLUGIN_BLACKLIST.includes(id)) + { + return false; + } + + return true; } \ No newline at end of file diff --git a/src/bun/utils/browser-params.ts b/src/bun/utils/browser-params.ts index 05efdf9..5059137 100644 --- a/src/bun/utils/browser-params.ts +++ b/src/bun/utils/browser-params.ts @@ -2,7 +2,6 @@ import { SERVER_URL } from "@shared/constants"; import os from 'node:os'; import path from 'node:path'; import { getBrowserPath } from "./get-browser"; -import { isSteamDeckGameMode } from "../utils"; import { ensureDir } from 'fs-extra'; import { host } from "./host"; @@ -11,6 +10,9 @@ export interface BrowserParams configPath: string; windowPosition?: { x: number, y: number; }; windowSize?: { width?: number, height?: number; }; + isSteamDeckGameMode: boolean; + forceBrowser?: boolean; + forceNWJS?: boolean; } export async function BuildParams (data: BrowserParams) @@ -41,7 +43,6 @@ export async function BuildParams (data: BrowserParams) args.push(`--app=${SERVER_URL(host)}`); args.push(`--app-id=gameflow`); - args.push(`--force-app-mode`); args.push('--no-default-browser-check'); args.push('--new-instance'); args.push('--no-first-run'); @@ -55,7 +56,14 @@ export async function BuildParams (data: BrowserParams) args.push('--allow-insecure-localhost'); args.push('--auto-accept-camera-and-microphone-capture'); - if (isSteamDeckGameMode()) + if (process.env.FLATPAK_BUILD) + { + args.push('--no-sandbox'); + args.push('--disable-gpu-sandbox'); + args.push('--test-type'); + } + + if (data.isSteamDeckGameMode) { args.push('--kiosk'); } else if (data.windowSize) diff --git a/src/bun/utils/browser-spawner.ts b/src/bun/utils/browser-spawner.ts index 005bf05..94d2801 100644 --- a/src/bun/utils/browser-spawner.ts +++ b/src/bun/utils/browser-spawner.ts @@ -1,8 +1,9 @@ import { $, type Subprocess } from "bun"; +import { ChildProcessWithoutNullStreams } from "node:child_process"; import os from 'node:os'; export type RunBrowserType = "chrome" | "chromium" | "firefox" | "edge"; -export type RunBrowserSource = "running" | "system" | "flatpak"; +export type RunBrowserSource = "running" | "system" | "flatpak" | "bundled"; /** * Options for spawning a browser process. @@ -27,11 +28,6 @@ interface SpawnBrowserOptions ipc?: (message: string) => void; } -interface SpawnLastInfo -{ - PID: number; -} - /** * Spawns a browser process with proper handling for different installation types. * @@ -168,21 +164,14 @@ export async function spawnBrowser ({ return processSub; } -export async function killBrowser (browser: Subprocess) +export async function killBrowser (browser: Subprocess | ChildProcessWithoutNullStreams) { if (os.platform() === 'linux') { - // kill chrome by your unique identifier - await $`pkill -KILL -P ${browser.pid}`.quiet().nothrow(); + // we have to force kill the demon spawn for some reason, doesn't respond to SIGTERM + await $`pkill -SIGKILL -P ${browser.pid}`.nothrow(); } else { - browser?.kill(15); + browser?.kill('SIGTERM'); } -} - -// --- Test Run --- -// spawnBrowser({ -// browser: "chrome", -// args: ["--window-size=1024,640", "--force-device-scale-factor=1.25"], -// detached: true -// }); \ No newline at end of file +} \ No newline at end of file diff --git a/src/bun/utils/downloader.ts b/src/bun/utils/downloader.ts new file mode 100644 index 0000000..920e7c8 --- /dev/null +++ b/src/bun/utils/downloader.ts @@ -0,0 +1,230 @@ +import { ensureDir } from "fs-extra"; +import path from 'node:path'; +import fs from 'node:fs/promises'; + +import { createWriteStream } from "node:fs"; +import { config, jar } from "../api/app"; +import { moveAllFiles } from "../utils"; +import { DownloadFileEntry, ProgressStats } from "@simeonradivoev/gameflow-sdk/shared"; + +interface TmpDownloadMetadata +{ + files: DownloadFileEntry[]; +} + +/** + * It download files and reports progress. + * It also automatically applies cookies from the jar store. + */ +export class Downloader +{ + files: DownloadFileEntry[]; + headers?: Record; + onProgress?: (stats: ProgressStats) => void; + signal?: AbortSignal; + activeFile?: DownloadFileEntry; + downloadPath: string | undefined; + id: string; + tmpPath: string; + tmpPathMeta: string; + downloadSpeed: number = 0; + + /** + * + * @param id Id of the download. Should be unique + * @param files All the files to download + * @param downloadPath The destination path when all downloads are complete they will bemoved here. If undefined they will remain in the tmp path. + */ + constructor( + id: string, + files: DownloadFileEntry[], + downloadPath: string | undefined, + init?: { + headers?: Record, + onProgress?: (stats: ProgressStats) => void; + signal?: AbortSignal; + }) + { + this.files = files; + this.headers = init?.headers; + this.onProgress = init?.onProgress; + this.signal = init?.signal; + this.downloadPath = downloadPath; + this.id = id; + this.tmpPath = path.join(config.get('downloadPath'), 'downloads', this.id); + this.tmpPathMeta = path.join(config.get('downloadPath'), 'downloads', `${this.id}.json`); + } + + async updateTmpDownload () + { + const meta: TmpDownloadMetadata = { + files: this.files + }; + + await ensureDir(path.join(config.get('downloadPath'), 'downloads')); + await fs.writeFile(this.tmpPathMeta, JSON.stringify(meta)); + } + + async start () + { + const totalSize = this.files.reduce((accum, current) => accum += current.size ?? 0, 0); + let bytesReceived = 0; + + if (this.files.some(f => path.isAbsolute(f.file_path))) + { + throw new Error("Only Relative Paths Supported"); + } + + await this.updateTmpDownload(); + + for (let i = 0; i < this.files.length; i++) + { + const file = this.files[i]; + this.activeFile = file; + const cookie = await jar.getCookieString(file.url.href); + + await ensureDir(path.join(this.tmpPath, file.file_path)); + + const filePath = path.join(this.tmpPath, file.file_path, file.file_name); + let start = 0; + + // 1. Check existing file + if (await fs.exists(filePath)) + { + start = ((await fs.stat(filePath)).size); + } + + // 2. Request remaining bytes + let res = await fetch(file.url, { + headers: { + ...this.headers, + ...(start > 0 + ? { Range: `bytes=${start}-` } + : undefined), + cookie + } + }); + + const resSize = Number(res.headers.get("content-length") ?? 0); + + if (start > 0) + { + if (res.status === 206) + { + console.log("Resume supported, continuing download"); + } else if (res.status === 200) + { + console.log("Server ignored Range, restarting download from beginning"); + start = 0; + + // Must make a new request from the beginning + res = await fetch(file.url, { headers: { ...this.headers, cookie } }); + + if (!res.ok) + { + throw new Error(`HTTP error: ${res.status} ${res.statusText}`); + } + } else if (res.status === 416) + { + const localSize = (await fs.stat(filePath)).size; + if (resSize && localSize === resSize) + { + console.log("File already fully downloaded, skipping"); + break; + } else + { + console.log("Partial file corrupt or changed, redownloading"); + start = 0; + res = await fetch(file.url, { headers: { ...this.headers, cookie } }); // full download + + if (!res.ok) + { + throw new Error(`HTTP error: ${res.status} ${res.statusText}`); + } + } + } + else + { + throw new Error(`HTTP error: ${res.status} ${res.statusText}`); + } + } else + { + if (!res.ok) throw new Error(`HTTP error: ${res.status} ${res.statusText}`); + } + + // 3. Append or overwrite + const stream = createWriteStream(filePath, { + flags: start > 0 ? "a" : "w", + highWaterMark: 64 * 1024 + }); + + const totalBytes = totalSize || Number(res.headers.get("content-length")) || 0; + bytesReceived += start; + + const reader = res.body!.getReader(); + + let lastUpdate = 0; + + while (true) + { + const { done, value } = await reader.read(); + if (done) break; + + bytesReceived += value.length; + if (totalBytes > 0 && this.onProgress) + { + const percent = (bytesReceived / totalBytes) * 100; + const timeDelta = Date.now() - lastUpdate; + if (timeDelta > 100) + { + this.downloadSpeed = this.downloadSpeed * 0.8 + Math.round(value.length / (timeDelta / 1000)) * 0.2; + this.onProgress({ progress: percent, downloaded: bytesReceived, total: totalBytes, speed: this.downloadSpeed }); + lastUpdate = Date.now(); + } + } + + if (this.signal?.aborted) + { + if (this.signal.reason === 'cancel') + { + console.log("Canceling Download and cleaning up files"); + await fs.rm(this.tmpPath, { recursive: true, maxRetries: 3, retryDelay: 3 }); + await fs.rm(this.tmpPathMeta); + return; + } + + reader.cancel(); + console.log("Aborting Download: ", this.signal.reason); + break; + } + + if (!stream.write(value)) + { + await new Promise((resolve) => stream.once("drain", () => resolve(true))); + } + } + + await new Promise((resolve, reject) => + { + stream.end(); + stream.on("close", () => resolve(false)); + stream.on("error", reject); + }); + } + + if (this.downloadPath === undefined) + { + await fs.rm(this.tmpPathMeta); + return this.files.map(f => path.join(this.tmpPath, f.file_path, f.file_name)); + } else + { + await moveAllFiles(this.tmpPath, this.downloadPath); + if (await fs.exists(this.tmpPath)) + await fs.rm(this.tmpPath, { recursive: true }); + await fs.rm(this.tmpPathMeta); + + return this.files.map(f => path.join(this.downloadPath!, f.file_path, f.file_name)); + } + + } +} \ No newline at end of file diff --git a/src/bun/utils/get-browser.ts b/src/bun/utils/get-browser.ts index 73370a2..7ba788e 100644 --- a/src/bun/utils/get-browser.ts +++ b/src/bun/utils/get-browser.ts @@ -1,9 +1,10 @@ -import { spawnSync } from "bun"; +import { Glob, spawnSync } from "bun"; import { platform } from "node:os"; import { RunBrowserType } from "./browser-spawner"; +import path from 'node:path'; export type GetBrowserType = "chrome" | "chromium" | "firefox"; -export type GetBrowserSource = "running" | "system" | "flatpak"; +export type GetBrowserSource = "running" | "system" | "flatpak" | "bundled"; /** * Browser discovery priority configuration @@ -12,6 +13,7 @@ interface BrowserPriorityConfig { /** Include currently running browser processes in search */ includeRunning?: boolean; + includeBundled?: boolean; /** Browser types to search for, in priority order */ browserOrder?: GetBrowserType[]; /** Include system default browser on Windows */ @@ -33,6 +35,20 @@ interface BrowserResult source: GetBrowserSource; } +/** The expected binary path per platform after extraction */ +async function getBundledBinaryPath (outDir: string, version: string, platform: string, arch: string): Promise +{ + let glob: Glob | undefined = undefined; + if (platform === "linux") glob = new Glob(`**/chrome`); + else if (platform === "darwin") glob = new Glob(`**/Chromium.app`); + else glob = new Glob(`**/chrome.exe`); + + for await (const bin of glob.scan({ cwd: outDir })) + { + return path.join(outDir, bin); + } +} + /** * Main function to find a valid browser executable. * @@ -63,6 +79,7 @@ export async function getBrowserPath (config?: BrowserPriorityConfig): Promisenul +echo Cleaning up... +rmdir /S /Q "{{{extractDir}}}" +del "{{{tempFile}}}" +echo Done! Restarting... +start "" /D "{{{installDir}}}" "{{{exePath}}}" +del "%~f0" \ No newline at end of file diff --git a/src/bun/webview/linux.ts b/src/bun/webview/linux.ts index c53ccca..6683249 100644 --- a/src/bun/webview/linux.ts +++ b/src/bun/webview/linux.ts @@ -1,36 +1,9 @@ import { Size, SizeHint, Webview } from 'webview-bun'; import webviewWorkerBase from "./base"; -if (process.env.FLATPAK_BUILD === "true") -{ - let webview: Bun.Subprocess | undefined = undefined; - let hostUrl: string | undefined = undefined; - webviewWorkerBase({ - navigate: (url) => - { - hostUrl = url; - - }, destroy: () => webview?.kill(), run: () => - { - webview = Bun.spawn(["webview", hostUrl ?? ''], { - stdout: "inherit", - stderr: "inherit", - env: { - ...process.env, - }, - onExit () - { - postMessage({ data: 'destroyed' }); - } - }); - } - }); -} else -{ - console.log("Launching Webview"); - let size: Size | undefined = undefined; - if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT) - size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE }; - const webview = new Webview(process.env.NODE_ENV === 'development', size); - webviewWorkerBase(webview); -} \ No newline at end of file +console.log("Launching Webview"); +let size: Size | undefined = undefined; +if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT) + size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE }; +const webview = new Webview(process.env.NODE_ENV === 'development', size); +webviewWorkerBase(webview); \ No newline at end of file diff --git a/src/bun/webview/win32.ts b/src/bun/webview/win32.ts index 6ef0a20..b575f57 100644 --- a/src/bun/webview/win32.ts +++ b/src/bun/webview/win32.ts @@ -6,4 +6,5 @@ let size: Size | undefined = undefined; if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT) size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE }; const webview = new Webview(process.env.NODE_ENV === 'development', size); +self.postMessage({ type: 'pointer', data: webview.unsafeWindowHandle }); webviewWorkerBase(webview); \ No newline at end of file diff --git a/src/clients/romm/@tanstack/react-query.gen.ts b/src/clients/romm/@tanstack/react-query.gen.ts index ae5aaab..3fa5c1e 100644 --- a/src/clients/romm/@tanstack/react-query.gen.ts +++ b/src/clients/romm/@tanstack/react-query.gen.ts @@ -3,8 +3,8 @@ import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; import { client } from '../client.gen'; -import { addCollectionApiCollectionsPost, addExclusionApiConfigExcludePost, addFirmwareApiFirmwarePost, addPlatformApiPlatformsPost, addPlatformBindingApiConfigSystemPlatformsPost, addPlatformVersionApiConfigSystemVersionsPost, addRomApiRomsPost, addRomManualsApiRomsIdManualsPost, addSaveApiSavesPost, addScreenshotApiScreenshotsPost, addSmartCollectionApiCollectionsSmartPost, addStateApiStatesPost, addUserApiUsersPost, authOpenidApiOauthOpenidGet, createInviteLinkApiUsersInviteLinkPost, createRomNoteApiRomsIdNotesPost, createSetupPlatformsApiSetupPlatformsPost, createUserFromInviteApiUsersRegisterPost, deleteCollectionApiCollectionsIdDelete, deleteExclusionApiConfigExcludeExclusionTypeExclusionValueDelete, deleteFirmwareApiFirmwareDeletePost, deletePlatformApiPlatformsIdDelete, deletePlatformBindingApiConfigSystemPlatformsFsSlugDelete, deletePlatformVersionApiConfigSystemVersionsFsSlugDelete, deleteRomManualsApiRomsIdManualsDelete, deleteRomNoteApiRomsIdNotesNoteIdDelete, deleteRomsApiRomsDeletePost, deleteSavesApiSavesDeletePost, deleteSmartCollectionApiCollectionsSmartIdDelete, deleteStatesApiStatesDeletePost, deleteUserApiUsersIdDelete, downloadRomsApiRomsDownloadGet, exportGamelistApiGamelistExportPost, fpkgiFeedApiFeedsFpkgiPlatformSlugGet, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getConfigApiConfigGet, getCurrentUserApiUsersMeGet, getFirmwareApiFirmwareIdGet, getFirmwareContentApiFirmwareIdContentFileNameGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRawAssetApiRawAssetsPathGet, getRomApiRomsIdGet, getRomByHashApiRomsByHashGet, getRomByMetadataProviderApiRomsByMetadataProviderGet, getRomContentApiRomsIdContentFileNameGet, getRomfileApiRomsFilesIdGet, getRomfileContentApiRomsfilesIdContentFileNameGet, getRomFiltersApiRomsFiltersGet, getRomNotesApiRomsIdNotesGet, getRomsApiRomsGet, getRoomsApiNetplayListGet, getSaveApiSavesIdGet, getSavesApiSavesGet, getSetupLibraryInfoApiSetupLibraryGet, getSmartCollectionApiCollectionsSmartIdGet, getSmartCollectionsApiCollectionsSmartGet, getStateApiStatesIdGet, getStatesApiStatesGet, getSupportedPlatformsEndpointApiPlatformsSupportedGet, getTaskByIdApiTasksTaskIdGet, getTasksStatusApiTasksStatusGet, getUserApiUsersIdGet, getUsersApiUsersGet, getVirtualCollectionApiCollectionsVirtualIdGet, getVirtualCollectionsApiCollectionsVirtualGet, heartbeatApiHeartbeatGet, kekatsuDsFeedApiFeedsKekatsuPlatformSlugGet, listTasksApiTasksGet, loginApiLoginPost, loginViaOpenidApiLoginOpenidGet, logoutApiLogoutPost, metadataHeartbeatApiHeartbeatMetadataSourceGet, type Options, pkgiPs3FeedApiFeedsPkgiPs3ContentTypeGet, pkgiPspFeedApiFeedsPkgiPspContentTypeGet, pkgiPsvitaFeedApiFeedsPkgiPsvitaContentTypeGet, platformsWebrcadeFeedApiFeedsWebrcadeGet, refreshRetroAchievementsApiUsersIdRaRefreshPost, requestPasswordResetApiForgotPasswordPost, resetPasswordApiResetPasswordPost, runAllTasksApiTasksRunPost, runSingleTaskApiTasksRunTaskNamePost, searchCoverApiSearchCoverGet, searchRomApiSearchRomsGet, statsApiStatsGet, tinfoilIndexFeedApiFeedsTinfoilGet, tokenApiTokenPost, updateCollectionApiCollectionsIdPut, updatePlatformApiPlatformsIdPut, updateRomApiRomsIdPut, updateRomNoteApiRomsIdNotesNoteIdPut, updateRomUserApiRomsIdPropsPut, updateSaveApiSavesIdPut, updateSmartCollectionApiCollectionsSmartIdPut, updateStateApiStatesIdPut, updateUserApiUsersIdPut } from '../sdk.gen'; -import type { AddCollectionApiCollectionsPostData, AddCollectionApiCollectionsPostError, AddCollectionApiCollectionsPostResponse, AddExclusionApiConfigExcludePostData, AddFirmwareApiFirmwarePostData, AddFirmwareApiFirmwarePostError, AddFirmwareApiFirmwarePostResponse, AddPlatformApiPlatformsPostData, AddPlatformApiPlatformsPostError, AddPlatformApiPlatformsPostResponse, AddPlatformBindingApiConfigSystemPlatformsPostData, AddPlatformVersionApiConfigSystemVersionsPostData, AddRomApiRomsPostData, AddRomApiRomsPostError, AddRomManualsApiRomsIdManualsPostData, AddRomManualsApiRomsIdManualsPostError, AddSaveApiSavesPostData, AddSaveApiSavesPostError, AddSaveApiSavesPostResponse, AddScreenshotApiScreenshotsPostData, AddScreenshotApiScreenshotsPostError, AddScreenshotApiScreenshotsPostResponse, AddSmartCollectionApiCollectionsSmartPostData, AddSmartCollectionApiCollectionsSmartPostError, AddSmartCollectionApiCollectionsSmartPostResponse, AddStateApiStatesPostData, AddStateApiStatesPostError, AddStateApiStatesPostResponse, AddUserApiUsersPostData, AddUserApiUsersPostError, AddUserApiUsersPostResponse, AuthOpenidApiOauthOpenidGetData, CreateInviteLinkApiUsersInviteLinkPostData, CreateInviteLinkApiUsersInviteLinkPostError, CreateInviteLinkApiUsersInviteLinkPostResponse, CreateRomNoteApiRomsIdNotesPostData, CreateRomNoteApiRomsIdNotesPostError, CreateRomNoteApiRomsIdNotesPostResponse, CreateSetupPlatformsApiSetupPlatformsPostData, CreateSetupPlatformsApiSetupPlatformsPostError, CreateUserFromInviteApiUsersRegisterPostData, CreateUserFromInviteApiUsersRegisterPostError, CreateUserFromInviteApiUsersRegisterPostResponse, DeleteCollectionApiCollectionsIdDeleteData, DeleteCollectionApiCollectionsIdDeleteError, DeleteExclusionApiConfigExcludeExclusionTypeExclusionValueDeleteData, DeleteExclusionApiConfigExcludeExclusionTypeExclusionValueDeleteError, DeleteFirmwareApiFirmwareDeletePostData, DeleteFirmwareApiFirmwareDeletePostError, DeleteFirmwareApiFirmwareDeletePostResponse, DeletePlatformApiPlatformsIdDeleteData, DeletePlatformApiPlatformsIdDeleteError, DeletePlatformBindingApiConfigSystemPlatformsFsSlugDeleteData, DeletePlatformBindingApiConfigSystemPlatformsFsSlugDeleteError, DeletePlatformVersionApiConfigSystemVersionsFsSlugDeleteData, DeletePlatformVersionApiConfigSystemVersionsFsSlugDeleteError, DeleteRomManualsApiRomsIdManualsDeleteData, DeleteRomManualsApiRomsIdManualsDeleteError, DeleteRomNoteApiRomsIdNotesNoteIdDeleteData, DeleteRomNoteApiRomsIdNotesNoteIdDeleteError, DeleteRomNoteApiRomsIdNotesNoteIdDeleteResponse, DeleteRomsApiRomsDeletePostData, DeleteRomsApiRomsDeletePostError, DeleteRomsApiRomsDeletePostResponse, DeleteSavesApiSavesDeletePostData, DeleteSavesApiSavesDeletePostError, DeleteSavesApiSavesDeletePostResponse, DeleteSmartCollectionApiCollectionsSmartIdDeleteData, DeleteSmartCollectionApiCollectionsSmartIdDeleteError, DeleteStatesApiStatesDeletePostData, DeleteStatesApiStatesDeletePostError, DeleteStatesApiStatesDeletePostResponse, DeleteUserApiUsersIdDeleteData, DeleteUserApiUsersIdDeleteError, DownloadRomsApiRomsDownloadGetData, DownloadRomsApiRomsDownloadGetError, ExportGamelistApiGamelistExportPostData, ExportGamelistApiGamelistExportPostError, FpkgiFeedApiFeedsFpkgiPlatformSlugGetData, FpkgiFeedApiFeedsFpkgiPlatformSlugGetError, GetCollectionApiCollectionsIdGetData, GetCollectionApiCollectionsIdGetError, GetCollectionApiCollectionsIdGetResponse, GetCollectionsApiCollectionsGetData, GetCollectionsApiCollectionsGetError, GetCollectionsApiCollectionsGetResponse, GetConfigApiConfigGetData, GetConfigApiConfigGetResponse, GetCurrentUserApiUsersMeGetData, GetCurrentUserApiUsersMeGetResponse, GetFirmwareApiFirmwareIdGetData, GetFirmwareApiFirmwareIdGetError, GetFirmwareApiFirmwareIdGetResponse, GetFirmwareContentApiFirmwareIdContentFileNameGetData, GetFirmwareContentApiFirmwareIdContentFileNameGetError, GetPlatformApiPlatformsIdGetData, GetPlatformApiPlatformsIdGetError, GetPlatformApiPlatformsIdGetResponse, GetPlatformFirmwareApiFirmwareGetData, GetPlatformFirmwareApiFirmwareGetError, GetPlatformFirmwareApiFirmwareGetResponse, GetPlatformsApiPlatformsGetData, GetPlatformsApiPlatformsGetError, GetPlatformsApiPlatformsGetResponse, GetRawAssetApiRawAssetsPathGetData, GetRawAssetApiRawAssetsPathGetError, GetRomApiRomsIdGetData, GetRomApiRomsIdGetError, GetRomApiRomsIdGetResponse, GetRomByHashApiRomsByHashGetData, GetRomByHashApiRomsByHashGetError, GetRomByHashApiRomsByHashGetResponse, GetRomByMetadataProviderApiRomsByMetadataProviderGetData, GetRomByMetadataProviderApiRomsByMetadataProviderGetError, GetRomByMetadataProviderApiRomsByMetadataProviderGetResponse, GetRomContentApiRomsIdContentFileNameGetData, GetRomContentApiRomsIdContentFileNameGetError, GetRomfileApiRomsFilesIdGetData, GetRomfileApiRomsFilesIdGetError, GetRomfileApiRomsFilesIdGetResponse, GetRomfileContentApiRomsfilesIdContentFileNameGetData, GetRomfileContentApiRomsfilesIdContentFileNameGetError, GetRomFiltersApiRomsFiltersGetData, GetRomFiltersApiRomsFiltersGetResponse, GetRomNotesApiRomsIdNotesGetData, GetRomNotesApiRomsIdNotesGetError, GetRomNotesApiRomsIdNotesGetResponse, GetRomsApiRomsGetData, GetRomsApiRomsGetError, GetRomsApiRomsGetResponse, GetRoomsApiNetplayListGetData, GetRoomsApiNetplayListGetError, GetRoomsApiNetplayListGetResponse, GetSaveApiSavesIdGetData, GetSaveApiSavesIdGetError, GetSaveApiSavesIdGetResponse, GetSavesApiSavesGetData, GetSavesApiSavesGetError, GetSavesApiSavesGetResponse, GetSetupLibraryInfoApiSetupLibraryGetData, GetSmartCollectionApiCollectionsSmartIdGetData, GetSmartCollectionApiCollectionsSmartIdGetError, GetSmartCollectionApiCollectionsSmartIdGetResponse, GetSmartCollectionsApiCollectionsSmartGetData, GetSmartCollectionsApiCollectionsSmartGetError, GetSmartCollectionsApiCollectionsSmartGetResponse, GetStateApiStatesIdGetData, GetStateApiStatesIdGetError, GetStateApiStatesIdGetResponse, GetStatesApiStatesGetData, GetStatesApiStatesGetError, GetStatesApiStatesGetResponse, GetSupportedPlatformsEndpointApiPlatformsSupportedGetData, GetSupportedPlatformsEndpointApiPlatformsSupportedGetResponse, GetTaskByIdApiTasksTaskIdGetData, GetTaskByIdApiTasksTaskIdGetError, GetTaskByIdApiTasksTaskIdGetResponse, GetTasksStatusApiTasksStatusGetData, GetTasksStatusApiTasksStatusGetResponse, GetUserApiUsersIdGetData, GetUserApiUsersIdGetError, GetUserApiUsersIdGetResponse, GetUsersApiUsersGetData, GetUsersApiUsersGetResponse, GetVirtualCollectionApiCollectionsVirtualIdGetData, GetVirtualCollectionApiCollectionsVirtualIdGetError, GetVirtualCollectionApiCollectionsVirtualIdGetResponse, GetVirtualCollectionsApiCollectionsVirtualGetData, GetVirtualCollectionsApiCollectionsVirtualGetError, GetVirtualCollectionsApiCollectionsVirtualGetResponse, HeartbeatApiHeartbeatGetData, HeartbeatApiHeartbeatGetResponse, KekatsuDsFeedApiFeedsKekatsuPlatformSlugGetData, KekatsuDsFeedApiFeedsKekatsuPlatformSlugGetError, ListTasksApiTasksGetData, ListTasksApiTasksGetResponse, LoginApiLoginPostData, LoginViaOpenidApiLoginOpenidGetData, LogoutApiLogoutPostData, MetadataHeartbeatApiHeartbeatMetadataSourceGetData, MetadataHeartbeatApiHeartbeatMetadataSourceGetError, MetadataHeartbeatApiHeartbeatMetadataSourceGetResponse, PkgiPs3FeedApiFeedsPkgiPs3ContentTypeGetData, PkgiPs3FeedApiFeedsPkgiPs3ContentTypeGetError, PkgiPspFeedApiFeedsPkgiPspContentTypeGetData, PkgiPspFeedApiFeedsPkgiPspContentTypeGetError, PkgiPsvitaFeedApiFeedsPkgiPsvitaContentTypeGetData, PkgiPsvitaFeedApiFeedsPkgiPsvitaContentTypeGetError, PlatformsWebrcadeFeedApiFeedsWebrcadeGetData, PlatformsWebrcadeFeedApiFeedsWebrcadeGetResponse, RefreshRetroAchievementsApiUsersIdRaRefreshPostData, RefreshRetroAchievementsApiUsersIdRaRefreshPostError, RequestPasswordResetApiForgotPasswordPostData, RequestPasswordResetApiForgotPasswordPostError, ResetPasswordApiResetPasswordPostData, ResetPasswordApiResetPasswordPostError, RunAllTasksApiTasksRunPostData, RunAllTasksApiTasksRunPostResponse, RunSingleTaskApiTasksRunTaskNamePostData, RunSingleTaskApiTasksRunTaskNamePostError, RunSingleTaskApiTasksRunTaskNamePostResponse, SearchCoverApiSearchCoverGetData, SearchCoverApiSearchCoverGetError, SearchCoverApiSearchCoverGetResponse, SearchRomApiSearchRomsGetData, SearchRomApiSearchRomsGetError, SearchRomApiSearchRomsGetResponse, StatsApiStatsGetData, StatsApiStatsGetResponse, TinfoilIndexFeedApiFeedsTinfoilGetData, TinfoilIndexFeedApiFeedsTinfoilGetError, TinfoilIndexFeedApiFeedsTinfoilGetResponse, TokenApiTokenPostData, TokenApiTokenPostError, TokenApiTokenPostResponse, UpdateCollectionApiCollectionsIdPutData, UpdateCollectionApiCollectionsIdPutError, UpdateCollectionApiCollectionsIdPutResponse, UpdatePlatformApiPlatformsIdPutData, UpdatePlatformApiPlatformsIdPutError, UpdatePlatformApiPlatformsIdPutResponse, UpdateRomApiRomsIdPutData, UpdateRomApiRomsIdPutError, UpdateRomApiRomsIdPutResponse, UpdateRomNoteApiRomsIdNotesNoteIdPutData, UpdateRomNoteApiRomsIdNotesNoteIdPutError, UpdateRomNoteApiRomsIdNotesNoteIdPutResponse, UpdateRomUserApiRomsIdPropsPutData, UpdateRomUserApiRomsIdPropsPutError, UpdateRomUserApiRomsIdPropsPutResponse, UpdateSaveApiSavesIdPutData, UpdateSaveApiSavesIdPutError, UpdateSaveApiSavesIdPutResponse, UpdateSmartCollectionApiCollectionsSmartIdPutData, UpdateSmartCollectionApiCollectionsSmartIdPutError, UpdateSmartCollectionApiCollectionsSmartIdPutResponse, UpdateStateApiStatesIdPutData, UpdateStateApiStatesIdPutError, UpdateStateApiStatesIdPutResponse, UpdateUserApiUsersIdPutData, UpdateUserApiUsersIdPutError, UpdateUserApiUsersIdPutResponse } from '../types.gen'; +import { addCollectionApiCollectionsPost, addExclusionApiConfigExcludePost, addFirmwareApiFirmwarePost, addPlatformApiPlatformsPost, addPlatformBindingApiConfigSystemPlatformsPost, addPlatformVersionApiConfigSystemVersionsPost, addRomApiRomsPost, addRomManualsApiRomsIdManualsPost, addSaveApiSavesPost, addScreenshotApiScreenshotsPost, addSmartCollectionApiCollectionsSmartPost, addStateApiStatesPost, addUserApiUsersPost, authOpenidApiOauthOpenidGet, confirmDownloadApiSavesIdDownloadedPost, createInviteLinkApiUsersInviteLinkPost, createRomNoteApiRomsIdNotesPost, createSetupPlatformsApiSetupPlatformsPost, createUserFromInviteApiUsersRegisterPost, deleteCollectionApiCollectionsIdDelete, deleteDeviceApiDevicesDeviceIdDelete, deleteExclusionApiConfigExcludeExclusionTypeExclusionValueDelete, deleteFirmwareApiFirmwareDeletePost, deletePlatformApiPlatformsIdDelete, deletePlatformBindingApiConfigSystemPlatformsFsSlugDelete, deletePlatformVersionApiConfigSystemVersionsFsSlugDelete, deleteRomManualsApiRomsIdManualsDelete, deleteRomNoteApiRomsIdNotesNoteIdDelete, deleteRomsApiRomsDeletePost, deleteSavesApiSavesDeletePost, deleteSmartCollectionApiCollectionsSmartIdDelete, deleteStatesApiStatesDeletePost, deleteUserApiUsersIdDelete, downloadRomsApiRomsDownloadGet, downloadSaveApiSavesIdContentGet, exportGamelistApiGamelistExportPost, fpkgiFeedApiFeedsFpkgiPlatformSlugGet, getCollectionApiCollectionsIdGet, getCollectionIdentifiersApiCollectionsIdentifiersGet, getCollectionsApiCollectionsGet, getConfigApiConfigGet, getCurrentUserApiUsersMeGet, getDeviceApiDevicesDeviceIdGet, getDevicesApiDevicesGet, getFirmwareApiFirmwareIdGet, getFirmwareContentApiFirmwareIdContentFileNameGet, getFirmwareIdentifiersApiFirmwareIdentifiersGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformIdentifiersApiPlatformsIdentifiersGet, getPlatformsApiPlatformsGet, getRawAssetApiRawAssetsPathGet, getRomApiRomsIdGet, getRomByHashApiRomsByHashGet, getRomByMetadataProviderApiRomsByMetadataProviderGet, getRomContentApiRomsIdContentFileNameGet, getRomfileApiRomsFilesIdGet, getRomfileContentApiRomsfilesIdContentFileNameGet, getRomFiltersApiRomsFiltersGet, getRomIdentifiersApiRomsIdentifiersGet, getRomNoteIdentifiersApiRomsIdNotesIdentifiersGet, getRomNotesApiRomsIdNotesGet, getRomsApiRomsGet, getRoomsApiNetplayListGet, getSaveApiSavesIdGet, getSaveIdentifiersApiSavesIdentifiersGet, getSavesApiSavesGet, getSavesSummaryApiSavesSummaryGet, getSetupLibraryInfoApiSetupLibraryGet, getSmartCollectionApiCollectionsSmartIdGet, getSmartCollectionIdentifiersApiCollectionsSmartIdentifiersGet, getSmartCollectionsApiCollectionsSmartGet, getStateApiStatesIdGet, getStateIdentifiersApiStatesIdentifiersGet, getStatesApiStatesGet, getSupportedPlatformsEndpointApiPlatformsSupportedGet, getTaskByIdApiTasksTaskIdGet, getTasksStatusApiTasksStatusGet, getUserApiUsersIdGet, getUserIdentifiersApiUsersIdentifiersGet, getUsersApiUsersGet, getVirtualCollectionApiCollectionsVirtualIdGet, getVirtualCollectionIdentifiersApiCollectionsVirtualIdentifiersGet, getVirtualCollectionsApiCollectionsVirtualGet, heartbeatApiHeartbeatGet, kekatsuDsFeedApiFeedsKekatsuPlatformSlugGet, listTasksApiTasksGet, loginApiLoginPost, loginViaOpenidApiLoginOpenidGet, logoutApiLogoutPost, metadataHeartbeatApiHeartbeatMetadataSourceGet, type Options, pkgiPs3FeedApiFeedsPkgiPs3ContentTypeGet, pkgiPspFeedApiFeedsPkgiPspContentTypeGet, pkgiPsvitaFeedApiFeedsPkgiPsvitaContentTypeGet, pkgjPspDlcsFeedApiFeedsPkgjPspDlcGet, pkgjPspGamesFeedApiFeedsPkgjPspGamesGet, pkgjPsvDlcsFeedApiFeedsPkgjPsvitaDlcGet, pkgjPsvGamesFeedApiFeedsPkgjPsvitaGamesGet, pkgjPsxGamesFeedApiFeedsPkgjPsxGamesGet, platformsWebrcadeFeedApiFeedsWebrcadeGet, refreshRetroAchievementsApiUsersIdRaRefreshPost, registerDeviceApiDevicesPost, requestPasswordResetApiForgotPasswordPost, resetPasswordApiResetPasswordPost, runAllTasksApiTasksRunPost, runSingleTaskApiTasksRunTaskNamePost, searchCoverApiSearchCoverGet, searchRomApiSearchRomsGet, statsApiStatsGet, tinfoilIndexFeedApiFeedsTinfoilGet, tokenApiTokenPost, trackSaveApiSavesIdTrackPost, untrackSaveApiSavesIdUntrackPost, updateCollectionApiCollectionsIdPut, updateDeviceApiDevicesDeviceIdPut, updatePlatformApiPlatformsIdPut, updateRomApiRomsIdPut, updateRomNoteApiRomsIdNotesNoteIdPut, updateRomUserApiRomsIdPropsPut, updateSaveApiSavesIdPut, updateSmartCollectionApiCollectionsSmartIdPut, updateStateApiStatesIdPut, updateUserApiUsersIdPut } from '../sdk.gen'; +import type { AddCollectionApiCollectionsPostData, AddCollectionApiCollectionsPostError, AddCollectionApiCollectionsPostResponse, AddExclusionApiConfigExcludePostData, AddFirmwareApiFirmwarePostData, AddFirmwareApiFirmwarePostError, AddFirmwareApiFirmwarePostResponse, AddPlatformApiPlatformsPostData, AddPlatformApiPlatformsPostError, AddPlatformApiPlatformsPostResponse, AddPlatformBindingApiConfigSystemPlatformsPostData, AddPlatformVersionApiConfigSystemVersionsPostData, AddRomApiRomsPostData, AddRomApiRomsPostError, AddRomManualsApiRomsIdManualsPostData, AddRomManualsApiRomsIdManualsPostError, AddSaveApiSavesPostData, AddSaveApiSavesPostError, AddSaveApiSavesPostResponse, AddScreenshotApiScreenshotsPostData, AddScreenshotApiScreenshotsPostError, AddScreenshotApiScreenshotsPostResponse, AddSmartCollectionApiCollectionsSmartPostData, AddSmartCollectionApiCollectionsSmartPostError, AddSmartCollectionApiCollectionsSmartPostResponse, AddStateApiStatesPostData, AddStateApiStatesPostError, AddStateApiStatesPostResponse, AddUserApiUsersPostData, AddUserApiUsersPostError, AddUserApiUsersPostResponse, AuthOpenidApiOauthOpenidGetData, ConfirmDownloadApiSavesIdDownloadedPostData, ConfirmDownloadApiSavesIdDownloadedPostError, ConfirmDownloadApiSavesIdDownloadedPostResponse, CreateInviteLinkApiUsersInviteLinkPostData, CreateInviteLinkApiUsersInviteLinkPostError, CreateInviteLinkApiUsersInviteLinkPostResponse, CreateRomNoteApiRomsIdNotesPostData, CreateRomNoteApiRomsIdNotesPostError, CreateRomNoteApiRomsIdNotesPostResponse, CreateSetupPlatformsApiSetupPlatformsPostData, CreateSetupPlatformsApiSetupPlatformsPostError, CreateUserFromInviteApiUsersRegisterPostData, CreateUserFromInviteApiUsersRegisterPostError, CreateUserFromInviteApiUsersRegisterPostResponse, DeleteCollectionApiCollectionsIdDeleteData, DeleteCollectionApiCollectionsIdDeleteError, DeleteDeviceApiDevicesDeviceIdDeleteData, DeleteDeviceApiDevicesDeviceIdDeleteError, DeleteDeviceApiDevicesDeviceIdDeleteResponse, DeleteExclusionApiConfigExcludeExclusionTypeExclusionValueDeleteData, DeleteExclusionApiConfigExcludeExclusionTypeExclusionValueDeleteError, DeleteFirmwareApiFirmwareDeletePostData, DeleteFirmwareApiFirmwareDeletePostError, DeleteFirmwareApiFirmwareDeletePostResponse, DeletePlatformApiPlatformsIdDeleteData, DeletePlatformApiPlatformsIdDeleteError, DeletePlatformBindingApiConfigSystemPlatformsFsSlugDeleteData, DeletePlatformBindingApiConfigSystemPlatformsFsSlugDeleteError, DeletePlatformVersionApiConfigSystemVersionsFsSlugDeleteData, DeletePlatformVersionApiConfigSystemVersionsFsSlugDeleteError, DeleteRomManualsApiRomsIdManualsDeleteData, DeleteRomManualsApiRomsIdManualsDeleteError, DeleteRomNoteApiRomsIdNotesNoteIdDeleteData, DeleteRomNoteApiRomsIdNotesNoteIdDeleteError, DeleteRomNoteApiRomsIdNotesNoteIdDeleteResponse, DeleteRomsApiRomsDeletePostData, DeleteRomsApiRomsDeletePostError, DeleteRomsApiRomsDeletePostResponse, DeleteSavesApiSavesDeletePostData, DeleteSavesApiSavesDeletePostError, DeleteSavesApiSavesDeletePostResponse, DeleteSmartCollectionApiCollectionsSmartIdDeleteData, DeleteSmartCollectionApiCollectionsSmartIdDeleteError, DeleteStatesApiStatesDeletePostData, DeleteStatesApiStatesDeletePostError, DeleteStatesApiStatesDeletePostResponse, DeleteUserApiUsersIdDeleteData, DeleteUserApiUsersIdDeleteError, DownloadRomsApiRomsDownloadGetData, DownloadRomsApiRomsDownloadGetError, DownloadSaveApiSavesIdContentGetData, DownloadSaveApiSavesIdContentGetError, ExportGamelistApiGamelistExportPostData, ExportGamelistApiGamelistExportPostError, FpkgiFeedApiFeedsFpkgiPlatformSlugGetData, FpkgiFeedApiFeedsFpkgiPlatformSlugGetError, GetCollectionApiCollectionsIdGetData, GetCollectionApiCollectionsIdGetError, GetCollectionApiCollectionsIdGetResponse, GetCollectionIdentifiersApiCollectionsIdentifiersGetData, GetCollectionIdentifiersApiCollectionsIdentifiersGetResponse, GetCollectionsApiCollectionsGetData, GetCollectionsApiCollectionsGetError, GetCollectionsApiCollectionsGetResponse, GetConfigApiConfigGetData, GetConfigApiConfigGetResponse, GetCurrentUserApiUsersMeGetData, GetCurrentUserApiUsersMeGetResponse, GetDeviceApiDevicesDeviceIdGetData, GetDeviceApiDevicesDeviceIdGetError, GetDeviceApiDevicesDeviceIdGetResponse, GetDevicesApiDevicesGetData, GetDevicesApiDevicesGetResponse, GetFirmwareApiFirmwareIdGetData, GetFirmwareApiFirmwareIdGetError, GetFirmwareApiFirmwareIdGetResponse, GetFirmwareContentApiFirmwareIdContentFileNameGetData, GetFirmwareContentApiFirmwareIdContentFileNameGetError, GetFirmwareIdentifiersApiFirmwareIdentifiersGetData, GetFirmwareIdentifiersApiFirmwareIdentifiersGetResponse, GetPlatformApiPlatformsIdGetData, GetPlatformApiPlatformsIdGetError, GetPlatformApiPlatformsIdGetResponse, GetPlatformFirmwareApiFirmwareGetData, GetPlatformFirmwareApiFirmwareGetError, GetPlatformFirmwareApiFirmwareGetResponse, GetPlatformIdentifiersApiPlatformsIdentifiersGetData, GetPlatformIdentifiersApiPlatformsIdentifiersGetResponse, GetPlatformsApiPlatformsGetData, GetPlatformsApiPlatformsGetError, GetPlatformsApiPlatformsGetResponse, GetRawAssetApiRawAssetsPathGetData, GetRawAssetApiRawAssetsPathGetError, GetRomApiRomsIdGetData, GetRomApiRomsIdGetError, GetRomApiRomsIdGetResponse, GetRomByHashApiRomsByHashGetData, GetRomByHashApiRomsByHashGetError, GetRomByHashApiRomsByHashGetResponse, GetRomByMetadataProviderApiRomsByMetadataProviderGetData, GetRomByMetadataProviderApiRomsByMetadataProviderGetError, GetRomByMetadataProviderApiRomsByMetadataProviderGetResponse, GetRomContentApiRomsIdContentFileNameGetData, GetRomContentApiRomsIdContentFileNameGetError, GetRomfileApiRomsFilesIdGetData, GetRomfileApiRomsFilesIdGetError, GetRomfileApiRomsFilesIdGetResponse, GetRomfileContentApiRomsfilesIdContentFileNameGetData, GetRomfileContentApiRomsfilesIdContentFileNameGetError, GetRomFiltersApiRomsFiltersGetData, GetRomFiltersApiRomsFiltersGetResponse, GetRomIdentifiersApiRomsIdentifiersGetData, GetRomIdentifiersApiRomsIdentifiersGetResponse, GetRomNoteIdentifiersApiRomsIdNotesIdentifiersGetData, GetRomNoteIdentifiersApiRomsIdNotesIdentifiersGetError, GetRomNoteIdentifiersApiRomsIdNotesIdentifiersGetResponse, GetRomNotesApiRomsIdNotesGetData, GetRomNotesApiRomsIdNotesGetError, GetRomNotesApiRomsIdNotesGetResponse, GetRomsApiRomsGetData, GetRomsApiRomsGetError, GetRomsApiRomsGetResponse, GetRoomsApiNetplayListGetData, GetRoomsApiNetplayListGetError, GetRoomsApiNetplayListGetResponse, GetSaveApiSavesIdGetData, GetSaveApiSavesIdGetError, GetSaveApiSavesIdGetResponse, GetSaveIdentifiersApiSavesIdentifiersGetData, GetSaveIdentifiersApiSavesIdentifiersGetResponse, GetSavesApiSavesGetData, GetSavesApiSavesGetError, GetSavesApiSavesGetResponse, GetSavesSummaryApiSavesSummaryGetData, GetSavesSummaryApiSavesSummaryGetError, GetSavesSummaryApiSavesSummaryGetResponse, GetSetupLibraryInfoApiSetupLibraryGetData, GetSmartCollectionApiCollectionsSmartIdGetData, GetSmartCollectionApiCollectionsSmartIdGetError, GetSmartCollectionApiCollectionsSmartIdGetResponse, GetSmartCollectionIdentifiersApiCollectionsSmartIdentifiersGetData, GetSmartCollectionIdentifiersApiCollectionsSmartIdentifiersGetResponse, GetSmartCollectionsApiCollectionsSmartGetData, GetSmartCollectionsApiCollectionsSmartGetError, GetSmartCollectionsApiCollectionsSmartGetResponse, GetStateApiStatesIdGetData, GetStateApiStatesIdGetError, GetStateApiStatesIdGetResponse, GetStateIdentifiersApiStatesIdentifiersGetData, GetStateIdentifiersApiStatesIdentifiersGetResponse, GetStatesApiStatesGetData, GetStatesApiStatesGetError, GetStatesApiStatesGetResponse, GetSupportedPlatformsEndpointApiPlatformsSupportedGetData, GetSupportedPlatformsEndpointApiPlatformsSupportedGetResponse, GetTaskByIdApiTasksTaskIdGetData, GetTaskByIdApiTasksTaskIdGetError, GetTaskByIdApiTasksTaskIdGetResponse, GetTasksStatusApiTasksStatusGetData, GetTasksStatusApiTasksStatusGetResponse, GetUserApiUsersIdGetData, GetUserApiUsersIdGetError, GetUserApiUsersIdGetResponse, GetUserIdentifiersApiUsersIdentifiersGetData, GetUserIdentifiersApiUsersIdentifiersGetResponse, GetUsersApiUsersGetData, GetUsersApiUsersGetResponse, GetVirtualCollectionApiCollectionsVirtualIdGetData, GetVirtualCollectionApiCollectionsVirtualIdGetError, GetVirtualCollectionApiCollectionsVirtualIdGetResponse, GetVirtualCollectionIdentifiersApiCollectionsVirtualIdentifiersGetData, GetVirtualCollectionIdentifiersApiCollectionsVirtualIdentifiersGetResponse, GetVirtualCollectionsApiCollectionsVirtualGetData, GetVirtualCollectionsApiCollectionsVirtualGetError, GetVirtualCollectionsApiCollectionsVirtualGetResponse, HeartbeatApiHeartbeatGetData, HeartbeatApiHeartbeatGetResponse, KekatsuDsFeedApiFeedsKekatsuPlatformSlugGetData, KekatsuDsFeedApiFeedsKekatsuPlatformSlugGetError, ListTasksApiTasksGetData, ListTasksApiTasksGetResponse, LoginApiLoginPostData, LoginViaOpenidApiLoginOpenidGetData, LogoutApiLogoutPostData, MetadataHeartbeatApiHeartbeatMetadataSourceGetData, MetadataHeartbeatApiHeartbeatMetadataSourceGetError, MetadataHeartbeatApiHeartbeatMetadataSourceGetResponse, PkgiPs3FeedApiFeedsPkgiPs3ContentTypeGetData, PkgiPs3FeedApiFeedsPkgiPs3ContentTypeGetError, PkgiPspFeedApiFeedsPkgiPspContentTypeGetData, PkgiPspFeedApiFeedsPkgiPspContentTypeGetError, PkgiPsvitaFeedApiFeedsPkgiPsvitaContentTypeGetData, PkgiPsvitaFeedApiFeedsPkgiPsvitaContentTypeGetError, PkgjPspDlcsFeedApiFeedsPkgjPspDlcGetData, PkgjPspGamesFeedApiFeedsPkgjPspGamesGetData, PkgjPsvDlcsFeedApiFeedsPkgjPsvitaDlcGetData, PkgjPsvGamesFeedApiFeedsPkgjPsvitaGamesGetData, PkgjPsxGamesFeedApiFeedsPkgjPsxGamesGetData, PlatformsWebrcadeFeedApiFeedsWebrcadeGetData, PlatformsWebrcadeFeedApiFeedsWebrcadeGetResponse, RefreshRetroAchievementsApiUsersIdRaRefreshPostData, RefreshRetroAchievementsApiUsersIdRaRefreshPostError, RegisterDeviceApiDevicesPostData, RegisterDeviceApiDevicesPostError, RegisterDeviceApiDevicesPostResponse, RequestPasswordResetApiForgotPasswordPostData, RequestPasswordResetApiForgotPasswordPostError, ResetPasswordApiResetPasswordPostData, ResetPasswordApiResetPasswordPostError, RunAllTasksApiTasksRunPostData, RunAllTasksApiTasksRunPostResponse, RunSingleTaskApiTasksRunTaskNamePostData, RunSingleTaskApiTasksRunTaskNamePostError, RunSingleTaskApiTasksRunTaskNamePostResponse, SearchCoverApiSearchCoverGetData, SearchCoverApiSearchCoverGetError, SearchCoverApiSearchCoverGetResponse, SearchRomApiSearchRomsGetData, SearchRomApiSearchRomsGetError, SearchRomApiSearchRomsGetResponse, StatsApiStatsGetData, StatsApiStatsGetResponse, TinfoilIndexFeedApiFeedsTinfoilGetData, TinfoilIndexFeedApiFeedsTinfoilGetError, TinfoilIndexFeedApiFeedsTinfoilGetResponse, TokenApiTokenPostData, TokenApiTokenPostError, TokenApiTokenPostResponse, TrackSaveApiSavesIdTrackPostData, TrackSaveApiSavesIdTrackPostError, TrackSaveApiSavesIdTrackPostResponse, UntrackSaveApiSavesIdUntrackPostData, UntrackSaveApiSavesIdUntrackPostError, UntrackSaveApiSavesIdUntrackPostResponse, UpdateCollectionApiCollectionsIdPutData, UpdateCollectionApiCollectionsIdPutError, UpdateCollectionApiCollectionsIdPutResponse, UpdateDeviceApiDevicesDeviceIdPutData, UpdateDeviceApiDevicesDeviceIdPutError, UpdateDeviceApiDevicesDeviceIdPutResponse, UpdatePlatformApiPlatformsIdPutData, UpdatePlatformApiPlatformsIdPutError, UpdatePlatformApiPlatformsIdPutResponse, UpdateRomApiRomsIdPutData, UpdateRomApiRomsIdPutError, UpdateRomApiRomsIdPutResponse, UpdateRomNoteApiRomsIdNotesNoteIdPutData, UpdateRomNoteApiRomsIdNotesNoteIdPutError, UpdateRomNoteApiRomsIdNotesNoteIdPutResponse, UpdateRomUserApiRomsIdPropsPutData, UpdateRomUserApiRomsIdPropsPutError, UpdateRomUserApiRomsIdPropsPutResponse, UpdateSaveApiSavesIdPutData, UpdateSaveApiSavesIdPutError, UpdateSaveApiSavesIdPutResponse, UpdateSmartCollectionApiCollectionsSmartIdPutData, UpdateSmartCollectionApiCollectionsSmartIdPutError, UpdateSmartCollectionApiCollectionsSmartIdPutResponse, UpdateStateApiStatesIdPutData, UpdateStateApiStatesIdPutError, UpdateStateApiStatesIdPutResponse, UpdateUserApiUsersIdPutData, UpdateUserApiUsersIdPutError, UpdateUserApiUsersIdPutResponse } from '../types.gen'; export type QueryKey = [ Pick & { @@ -442,6 +442,32 @@ export const createUserFromInviteApiUsersRegisterPostMutation = (options?: Parti return mutationOptions; }; +export const getUserIdentifiersApiUsersIdentifiersGetQueryKey = (options?: Options) => createQueryKey('getUserIdentifiersApiUsersIdentifiersGet', options); + +/** + * Get User Identifiers + * + * Get all user identifiers endpoint + * + * Args: + * request (Request): Fastapi Request object + * + * Returns: + * list[int]: All user ids stored in the RomM's database + */ +export const getUserIdentifiersApiUsersIdentifiersGetOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUserIdentifiersApiUsersIdentifiersGet({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserIdentifiersApiUsersIdentifiersGetQueryKey(options) +}); + export const getCurrentUserApiUsersMeGetQueryKey = (options?: Options) => createQueryKey('getCurrentUserApiUsersMeGet', options); /** @@ -568,6 +594,93 @@ export const refreshRetroAchievementsApiUsersIdRaRefreshPostMutation = (options? return mutationOptions; }; +export const getDevicesApiDevicesGetQueryKey = (options?: Options) => createQueryKey('getDevicesApiDevicesGet', options); + +/** + * Get Devices + */ +export const getDevicesApiDevicesGetOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getDevicesApiDevicesGet({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getDevicesApiDevicesGetQueryKey(options) +}); + +/** + * Register Device + */ +export const registerDeviceApiDevicesPostMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await registerDeviceApiDevicesPost({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Delete Device + */ +export const deleteDeviceApiDevicesDeviceIdDeleteMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteDeviceApiDevicesDeviceIdDelete({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getDeviceApiDevicesDeviceIdGetQueryKey = (options: Options) => createQueryKey('getDeviceApiDevicesDeviceIdGet', options); + +/** + * Get Device + */ +export const getDeviceApiDevicesDeviceIdGetOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getDeviceApiDevicesDeviceIdGet({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getDeviceApiDevicesDeviceIdGetQueryKey(options) +}); + +/** + * Update Device + */ +export const updateDeviceApiDevicesDeviceIdPutMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateDeviceApiDevicesDeviceIdPut({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const getPlatformsApiPlatformsGetQueryKey = (options?: Options) => createQueryKey('getPlatformsApiPlatformsGet', options); /** @@ -607,6 +720,26 @@ export const addPlatformApiPlatformsPostMutation = (options?: Partial) => createQueryKey('getPlatformIdentifiersApiPlatformsIdentifiersGet', options); + +/** + * Get Platform Identifiers + * + * Retrieve platform identifiers. + */ +export const getPlatformIdentifiersApiPlatformsIdentifiersGetOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getPlatformIdentifiersApiPlatformsIdentifiersGet({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getPlatformIdentifiersApiPlatformsIdentifiersGetQueryKey(options) +}); + export const getSupportedPlatformsEndpointApiPlatformsSupportedGetQueryKey = (options?: Options) => createQueryKey('getSupportedPlatformsEndpointApiPlatformsSupportedGet', options); /** @@ -782,6 +915,26 @@ export const addRomApiRomsPostMutation = (options?: Partial) => createQueryKey('getRomIdentifiersApiRomsIdentifiersGet', options); + +/** + * Get Rom Identifiers + * + * Retrieve rom identifiers. + */ +export const getRomIdentifiersApiRomsIdentifiersGetOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getRomIdentifiersApiRomsIdentifiersGet({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getRomIdentifiersApiRomsIdentifiersGetQueryKey(options) +}); + export const downloadRomsApiRomsDownloadGetQueryKey = (options: Options) => createQueryKey('downloadRomsApiRomsDownloadGet', options); /** @@ -1076,6 +1229,26 @@ export const createRomNoteApiRomsIdNotesPostMutation = (options?: Partial