From ac34e17438e3f10be59af7263508e22528642c79 Mon Sep 17 00:00:00 2001 From: phantomptr Date: Wed, 17 Jun 2026 00:39:29 -0700 Subject: [PATCH] feat(installed): show running game + Stop, and a patient launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Installed Apps improvements requested from real use: 1. Detect a running game + stop it. A new poll (reusing the existing rich process list — title_id/app_id/kind, same data the Processes screen uses) marks the running title with a "Playing" badge and turns its Play button into "Close game" (confirmed; appKill by app id, processKill(pid) fallback). Read-only detection — it never touches a running/starting game; stopping is always explicit. 2. Patient launch. Pressing Play now holds a "Starting…" state and watches (read-only) for the title to actually appear before declaring it playing — first/cold launches can be slow. Play is disabled during this window so a second click can't fire a launch into a still-starting game (which is how it gets knocked back down). A slow start shows a calm "give it a moment" note, never an error, and never a kill. Degrades gracefully: if the process list can't report a title (older payload/FW), cards just show Play as before — nothing breaks. Client-only; processList/appKill/processKill already existed. +4 helper tests; 743 green; new strings added to en.ts + allowlist. HW: launch verified on a real PS5 (Phat, FW 5.10) — eboot.bin came up after appLaunch ok. Release 3.3.22. --- CHANGELOG.md | 13 + VERSION | 2 +- client/package-lock.json | 4 +- client/package.json | 2 +- client/src-tauri/Cargo.toml | 2 +- client/src-tauri/tauri.conf.json | 2 +- client/src/i18n/locales/en.ts | 13 + client/src/lib/runningGames.test.ts | 81 ++++++ client/src/lib/runningGames.ts | 50 ++++ client/src/screens/InstalledApps/index.tsx | 279 +++++++++++++++++---- engine/Cargo.lock | 14 +- engine/Cargo.toml | 2 +- payload/include/config.h | 2 +- scripts/i18n-known-missing.json | 187 ++++++++++++++ 14 files changed, 594 insertions(+), 59 deletions(-) create mode 100644 client/src/lib/runningGames.test.ts create mode 100644 client/src/lib/runningGames.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d742fbc..f56a5679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ What's new in ps5upload, written for humans. --- +## 3.3.22 + +- **See what's playing, and stop it.** Installed Apps now shows a "Playing" badge + on whatever game is currently running, and the Play button turns into **Close + game** so you can stop it right from the app (with a confirm — it's the same + as quitting on the console). +- **More patience when launching a game.** A first launch (just-installed, or a + cold start) can take a while to come up. Pressing Play now shows "Starting…" + and waits for the game to actually appear before saying it's playing — and it + won't let you fire a second launch into a game that's still starting (which is + what could knock it back down). If it's taking a while, you get a calm "give + it a moment" note rather than an error. + ## 3.3.21 - **Fixed: moving files from USB to the internal SSD crashed the console.** Cut diff --git a/VERSION b/VERSION index b2847d51..c21e5b4e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.21 +3.3.22 diff --git a/client/package-lock.json b/client/package-lock.json index 5d402a01..9973b481 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "ps5upload-client", - "version": "3.3.21", + "version": "3.3.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ps5upload-client", - "version": "3.3.21", + "version": "3.3.22", "dependencies": { "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-dialog": "^2.7.1", diff --git a/client/package.json b/client/package.json index 7c0b28ed..4385f0e0 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "ps5upload-client", "private": true, - "version": "3.3.21", + "version": "3.3.22", "description": "The all-in-one PS5 companion app.", "homepage": "https://github.com/phantomptr/ps5upload", "author": "PhantomPtr ", diff --git a/client/src-tauri/Cargo.toml b/client/src-tauri/Cargo.toml index d7215756..1f10fb1a 100644 --- a/client/src-tauri/Cargo.toml +++ b/client/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ps5upload-desktop" -version = "3.3.21" +version = "3.3.22" description = "The all-in-one PS5 companion app." edition = "2021" rust-version = "1.77" diff --git a/client/src-tauri/tauri.conf.json b/client/src-tauri/tauri.conf.json index ced616c2..f87f1b98 100644 --- a/client/src-tauri/tauri.conf.json +++ b/client/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "PS5Upload", - "version": "3.3.21", + "version": "3.3.22", "identifier": "com.phantomptr.ps5upload", "build": { "beforeDevCommand": "npm run dev:vite", diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index cf28d8e4..2e92e24c 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -1616,6 +1616,19 @@ installed_play: "Play", installed_play_tooltip: "Launch this title on the PS5", installed_launching: "Launching…", installed_launch_sent: "Launch sent — check your PS5", +installed_starting: "Starting…", +installed_now_playing: "Now playing", +installed_badge_playing: "Playing", +installed_launch_slow: + "Launch sent — first starts can take a while. Give it a moment and check your PS5.", +installed_stop: "Close game", +installed_stopping: "Closing…", +installed_stop_tooltip: "Close this running game on the PS5", +installed_stop_confirm_title: "Close {name}?", +installed_stop_confirm_body: + "This closes the running game on the PS5. Any unsaved progress will be lost — the same as quitting from the console.", +installed_stopped: "Game closed", +installed_stop_failed: "Couldn't close the game — it may have already exited.", installed_disc_needs_smp_row: "Needs ShadowMount+ running to mount + launch.", installed_kstuff_off_title: "kstuff isn't active — games won't launch", installed_kstuff_off_body: diff --git a/client/src/lib/runningGames.test.ts b/client/src/lib/runningGames.test.ts new file mode 100644 index 00000000..e0eef6e2 --- /dev/null +++ b/client/src/lib/runningGames.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +vi.mock("../api/ps5", () => ({ processList: vi.fn() })); + +import { processList, type ProcessInfo } from "../api/ps5"; +import { fetchRunningGames } from "./runningGames"; + +const mockedList = vi.mocked(processList); + +function proc(p: Partial): ProcessInfo { + return { + pid: 0, + name: "", + comm: "", + title_id: "", + app_id: 0, + memory_mib: 0, + threads: 1, + kind: "app", + ...p, + }; +} + +describe("fetchRunningGames", () => { + beforeEach(() => mockedList.mockReset()); + + it("collapses a title's many processes into one running entry", async () => { + mockedList.mockResolvedValue({ + truncated: false, + processes: [ + proc({ pid: 100, title_id: "CUSA00900", app_id: 0, comm: "eboot.bin" }), + proc({ pid: 101, title_id: "CUSA00900", app_id: 42, comm: "GnmCompositor" }), + proc({ pid: 102, title_id: "CUSA00900", app_id: 0, comm: "AudioOut" }), + ], + }); + const m = await fetchRunningGames("ip:9114"); + expect(m.size).toBe(1); + // Prefers the process that carries a real app id (for a clean appKill). + expect(m.get("CUSA00900")).toEqual({ + titleId: "CUSA00900", + appId: 42, + pid: 101, + }); + }); + + it("ignores the helper itself, payload, and system processes", async () => { + mockedList.mockResolvedValue({ + truncated: false, + processes: [ + proc({ pid: 1, title_id: "CUSA00001", kind: "app", is_self: true }), + proc({ pid: 2, title_id: "", kind: "payload", comm: "ps5upload" }), + proc({ pid: 3, title_id: "NPXS40000", kind: "system" }), + proc({ pid: 4, title_id: "PPSA01234", kind: "app", app_id: 7 }), + ], + }); + const m = await fetchRunningGames("ip:9114"); + expect([...m.keys()]).toEqual(["PPSA01234"]); + }); + + it("returns empty when nothing game-like is running", async () => { + mockedList.mockResolvedValue({ + truncated: false, + processes: [proc({ pid: 2, kind: "payload", comm: "ps5upload" })], + }); + const m = await fetchRunningGames("ip:9114"); + expect(m.size).toBe(0); + }); + + it("falls back to the pid when a title has no app id", async () => { + mockedList.mockResolvedValue({ + truncated: false, + processes: [proc({ pid: 55, title_id: "CUSA07842", app_id: 0 })], + }); + const m = await fetchRunningGames("ip:9114"); + expect(m.get("CUSA07842")).toEqual({ + titleId: "CUSA07842", + appId: 0, + pid: 55, + }); + }); +}); diff --git a/client/src/lib/runningGames.ts b/client/src/lib/runningGames.ts new file mode 100644 index 00000000..f647c467 --- /dev/null +++ b/client/src/lib/runningGames.ts @@ -0,0 +1,50 @@ +import { processList } from "../api/ps5"; + +/** A game/app currently running on the console, distilled from the process + * list down to what the Installed Apps screen needs to show "Playing" and to + * stop it. */ +export interface RunningGame { + titleId: string; + /** Sony app id — preferred handle for a clean stop via appKill(). 0 when the + * process list didn't carry one (fall back to the pid). */ + appId: number; + /** A pid belonging to the title — the processKill() fallback when there's no + * app id. */ + pid: number; +} + +/** + * Map `title_id` → running game, from the console's process list. + * + * A running game spawns many threads/processes that all share one title_id; + * we collapse them to a single entry per title, preferring a process that + * carries a real Sony app id (so a later appKill() has a clean handle). Only + * actual app/game processes count — never the PS5Upload helper itself + * (`is_self`), the payload, or system processes — so the running indicator and + * the Stop button can't target our own connection or the OS. + * + * Read-only: this never touches a running game; detecting one must not risk + * disturbing a title that's still coming up. + */ +export async function fetchRunningGames( + mgmtAddr: string, +): Promise> { + const res = await processList(mgmtAddr); + const byTitle = new Map(); + for (const p of res.processes) { + if (p.kind !== "app" || !p.title_id || p.is_self) continue; + const existing = byTitle.get(p.title_id); + if (!existing) { + byTitle.set(p.title_id, { + titleId: p.title_id, + appId: p.app_id || 0, + pid: p.pid, + }); + } else if (!existing.appId && p.app_id) { + // Upgrade to a process that has a real app id for a cleaner kill. + existing.appId = p.app_id; + existing.pid = p.pid; + } + } + return byTitle; +} diff --git a/client/src/screens/InstalledApps/index.tsx b/client/src/screens/InstalledApps/index.tsx index 796f7a77..1fe6f540 100644 --- a/client/src/screens/InstalledApps/index.tsx +++ b/client/src/screens/InstalledApps/index.tsx @@ -12,6 +12,8 @@ import { Loader2, Download, ShieldCheck, + Square, + CircleDot, } from "lucide-react"; import { useNavigate } from "react-router-dom"; @@ -22,10 +24,16 @@ import { appUnregister, appLaunch, appIconUrl, + appKill, + processKill, smpStatus, type InstalledTitle, type SmpStatus, } from "../../api/ps5"; +import { + fetchRunningGames, + type RunningGame, +} from "../../lib/runningGames"; import { PageHeader, EmptyState, @@ -144,6 +152,14 @@ function Cover({ host, title }: { host: string; title: InstalledTitle }) { ); } +// How long to keep a just-launched title in the patient "Starting…" state +// while we watch for its process to appear, and how often to check. A first +// launch (just-installed, cold cache, disc image) can be slow, so this is +// generous — it never fails the launch or touches the game, it just stops +// reporting "Starting…" after this and tells the user it may still be coming up. +const LAUNCH_CONFIRM_TIMEOUT_MS = 90_000; +const LAUNCH_CONFIRM_POLL_MS = 2_000; + // ── App card ───────────────────────────────────────────────────────────────── function AppCard({ @@ -151,18 +167,26 @@ function AppCard({ title, busy, launching, + running, + stopping, discNeedsSmp, onUninstall, onLaunch, + onStop, }: { host: string; title: InstalledTitle; busy: boolean; launching: boolean; + /** True while this title is running on the console (show Stop + "Playing"). */ + running: boolean; + /** True while a Stop request for this title is in flight. */ + stopping: boolean; /** True for a disc-image title while ShadowMount+ isn't running. */ discNeedsSmp: boolean; onUninstall: (t: InstalledTitle) => void; onLaunch: (t: InstalledTitle) => void; + onStop: (t: InstalledTitle) => void; }) { const tr = useTr(); const navigate = useNavigate(); @@ -193,6 +217,15 @@ function AppCard({ {tr("installed_badge_smp_needed", undefined, "SMP")} ) : null} + {running ? ( + + + {tr("installed_badge_playing", undefined, "Playing")} + + ) : null} {/* Body: name → type + id → actions pinned to the bottom (mt-auto) so @@ -225,42 +258,66 @@ function AppCard({
{canPlay ? ( - + running && !launching ? ( + // The title is running → offer Stop (close the game) instead of + // Play. Stop is the ONLY thing that ends a game, and it's always + // explicit + confirmed — we never auto-close a running or + // starting title. + + ) : ( + + ) ) : ( {tr("installed_badge_system", undefined, "System")} @@ -363,6 +420,10 @@ export default function InstalledAppsScreen() { const [registeredUnavailable, setRegisteredUnavailable] = useState(false); const [busyId, setBusyId] = useState(null); const [launchingId, setLaunchingId] = useState(null); + // Games currently running on the console (title_id → handle), refreshed on a + // poll while this screen is open. Drives the "Playing" badge + Stop button. + const [running, setRunning] = useState>(new Map()); + const [stoppingId, setStoppingId] = useState(null); const [smpSending, setSmpSending] = useState(false); const [smpMsg, setSmpMsg] = useState(null); // Native window.confirm() is a no-op in Tauri's webview; use the in-tree @@ -406,28 +467,90 @@ export default function InstalledAppsScreen() { setTitles(null); setSmp("checking"); setError(null); + setRunning(new Map()); }, [host]); useEffect(() => { void refresh(); }, [refresh]); + // Poll which games are running so cards can show "Playing" + a Stop button, + // and a just-launched title flips from "Starting…" to "Playing" on its own. + // Read-only (process list) — it never touches a running game, so it can't + // disturb a title that's still coming up. Runs only while the screen is open. + useEffect(() => { + if (!host?.trim()) return; + let cancelled = false; + const addr = mgmtAddr(host); + const tick = async () => { + try { + const r = await fetchRunningGames(addr); + if (!cancelled) setRunning(r); + } catch { + // Transient (console busy/offline) — keep the last known set rather + // than flicker every card's state on one failed poll. + } + }; + void tick(); + const id = setInterval(() => void tick(), 3000); + return () => { + cancelled = true; + clearInterval(id); + }; + }, [host]); + const handleLaunch = useCallback( async (t: InstalledTitle) => { if (!host?.trim()) return; const probe = guard.capture(); + // Hold the "Starting…" state for the whole come-up window (not just the + // launch RPC) so Play stays disabled and the user can't fire a second + // launch into a game that's still starting — re-launching a half-started + // title is exactly how it gets killed. We only watch (read-only) for the + // game's process to appear; we never act on a starting game. setLaunchingId(t.titleId); try { await appLaunch(transferAddr(probe.host), t.titleId); if (probe.isStale()) return; - // Toast (not inline) so the card grid stays a uniform height. - pushNotification("info", withConsolePrefix(probe.host, t.titleName), { - body: tr( - "installed_launch_sent", - undefined, - "Launch sent — check your PS5", - ), - }); + // Patiently wait for the title to actually come up. A first launch + // (just-installed, cold cache, disc image) can take a while; the launch + // RPC only means "Sony accepted it," not "it's running." Poll the + // process list until the title appears, then the card shows "Playing". + const addr = mgmtAddr(probe.host); + const deadline = Date.now() + LAUNCH_CONFIRM_TIMEOUT_MS; + let confirmed = false; + while (Date.now() < deadline) { + await new Promise((r) => setTimeout(r, LAUNCH_CONFIRM_POLL_MS)); + if (probe.isStale()) return; + try { + const r = await fetchRunningGames(addr); + if (probe.isStale()) return; + setRunning(r); + if (r.has(t.titleId)) { + confirmed = true; + break; + } + } catch { + // Console momentarily busy/recovering while the game spins up — + // keep waiting rather than giving up. + } + } + if (confirmed) { + pushNotification("success", withConsolePrefix(probe.host, t.titleName), { + body: tr("installed_now_playing", undefined, "Now playing"), + }); + } else { + // Not seen yet — do NOT treat this as a failure (and never kill it). + // The launch was accepted; a slow first start just hasn't surfaced in + // the process list yet. + pushNotification("info", withConsolePrefix(probe.host, t.titleName), { + body: tr( + "installed_launch_slow", + undefined, + "Launch sent — first starts can take a while. Give it a moment and check your PS5.", + ), + }); + } } catch (e) { if (probe.isStale()) return; const raw = e instanceof Error ? e.message : String(e); @@ -441,6 +564,71 @@ export default function InstalledAppsScreen() { [host, guard, tr], ); + const handleStop = useCallback( + async (t: InstalledTitle) => { + if (!host?.trim()) return; + const game = running.get(t.titleId); + if (!game) return; + const probe = guard.capture(); + const ok = await confirmDialog({ + title: tr( + "installed_stop_confirm_title", + { name: t.titleName }, + `Close ${t.titleName}?`, + ), + message: tr( + "installed_stop_confirm_body", + undefined, + "This closes the running game on the PS5. Any unsaved progress will be lost — the same as quitting from the console.", + ), + confirmLabel: tr("installed_stop", undefined, "Close game"), + destructive: true, + }); + if (!ok || probe.isStale()) return; + setStoppingId(t.titleId); + try { + // Prefer Sony's clean app-kill (by app id); fall back to SIGKILL of a + // pid for the title when no app id was reported. + const addr = mgmtAddr(probe.host); + let ack = game.appId ? await appKill(addr, game.appId) : { ok: false }; + if (!ack.ok && game.pid) { + const k = await processKill(addr, game.pid); + ack = { ok: k.ok }; + } + if (probe.isStale()) return; + if (ack.ok) { + // Drop it from the running set immediately so the card flips back to + // Play without waiting for the next poll. + setRunning((cur) => { + const next = new Map(cur); + next.delete(t.titleId); + return next; + }); + pushNotification("info", withConsolePrefix(probe.host, t.titleName), { + body: tr("installed_stopped", undefined, "Game closed"), + }); + } else { + pushNotification("error", withConsolePrefix(probe.host, t.titleName), { + body: tr( + "installed_stop_failed", + undefined, + "Couldn't close the game — it may have already exited.", + ), + }); + } + } catch (e) { + if (probe.isStale()) return; + const raw = e instanceof Error ? e.message : String(e); + pushNotification("error", withConsolePrefix(probe.host, t.titleName), { + body: humanizePs5Error(raw), + }); + } finally { + setStoppingId(null); + } + }, + [host, guard, running, confirmDialog, tr], + ); + const handleUninstall = useCallback( async (t: InstalledTitle) => { if (!host?.trim()) return; @@ -570,9 +758,12 @@ export default function InstalledAppsScreen() { title: t, busy: busyId === t.titleId, launching: launchingId === t.titleId, + running: running.has(t.titleId), + stopping: stoppingId === t.titleId, discNeedsSmp: kindOf(t) === "disc" && !smpRunning && !smpChecking, onUninstall: handleUninstall, onLaunch: handleLaunch, + onStop: handleStop, }); return ( diff --git a/engine/Cargo.lock b/engine/Cargo.lock index a123c2c0..bcab459e 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -494,7 +494,7 @@ dependencies = [ [[package]] name = "ftx2-proto" -version = "3.3.21" +version = "3.3.22" dependencies = [ "serde", "thiserror", @@ -954,7 +954,7 @@ dependencies = [ [[package]] name = "ps5upload-bench" -version = "3.3.21" +version = "3.3.22" dependencies = [ "criterion", "ftx2-proto", @@ -964,7 +964,7 @@ dependencies = [ [[package]] name = "ps5upload-core" -version = "3.3.21" +version = "3.3.22" dependencies = [ "anyhow", "base64", @@ -981,7 +981,7 @@ dependencies = [ [[package]] name = "ps5upload-engine" -version = "3.3.21" +version = "3.3.22" dependencies = [ "anyhow", "axum", @@ -999,7 +999,7 @@ dependencies = [ [[package]] name = "ps5upload-lab" -version = "3.3.21" +version = "3.3.22" dependencies = [ "anyhow", "ftx2-proto", @@ -1008,7 +1008,7 @@ dependencies = [ [[package]] name = "ps5upload-pkg" -version = "3.3.21" +version = "3.3.22" dependencies = [ "serde", "serde_json", @@ -1017,7 +1017,7 @@ dependencies = [ [[package]] name = "ps5upload-tests" -version = "3.3.21" +version = "3.3.22" dependencies = [ "anyhow", "ftx2-proto", diff --git a/engine/Cargo.toml b/engine/Cargo.toml index eb2a00c7..5c539a8b 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -13,7 +13,7 @@ resolver = "2" [workspace.package] edition = "2021" license = "GPL-3.0-or-later" -version = "3.3.21" +version = "3.3.22" [workspace.dependencies] anyhow = "1.0" diff --git a/payload/include/config.h b/payload/include/config.h index 56726cfc..493089e0 100644 --- a/payload/include/config.h +++ b/payload/include/config.h @@ -5,7 +5,7 @@ * UI tell apart an old payload still running from a build that includes * a particular fix, without having to boot the console. Keep in sync * with the desktop app's package.json during releases. */ -#define PS5UPLOAD2_VERSION "3.3.21" +#define PS5UPLOAD2_VERSION "3.3.22" /* Author credit — embedded in the startup toast so anyone looking at * the console screen knows who wrote the software that just loaded. * Kept separate from VERSION so release scripts can bump the version diff --git a/scripts/i18n-known-missing.json b/scripts/i18n-known-missing.json index 2572c030..92cdc455 100644 --- a/scripts/i18n-known-missing.json +++ b/scripts/i18n-known-missing.json @@ -47,7 +47,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -232,7 +243,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -417,7 +439,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -602,7 +635,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -787,7 +831,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -972,7 +1027,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -1157,7 +1223,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -1342,7 +1419,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -1527,7 +1615,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -1712,7 +1811,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -1897,7 +2007,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -2082,7 +2203,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -2267,7 +2399,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -2452,7 +2595,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -2637,7 +2791,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -2822,7 +2987,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count", @@ -3007,7 +3183,18 @@ "err_rar_unsupported", "fs_op_on_other_console", "fs_rename_target_exists", + "installed_badge_playing", + "installed_launch_slow", + "installed_now_playing", "installed_open_folder", + "installed_starting", + "installed_stop", + "installed_stop_confirm_body", + "installed_stop_confirm_title", + "installed_stop_failed", + "installed_stop_tooltip", + "installed_stopped", + "installed_stopping", "library_show_all", "pkglib.external.autoScan", "pkglib.external.count",