Skip to content

Commit 20721cc

Browse files
Merge pull request #11 from ForWard-Technologies-LLC/release/beta
Release/beta
2 parents c619301 + 3cc830a commit 20721cc

53 files changed

Lines changed: 3091 additions & 748 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-flatpak.yml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
# AppStream release history (gui/*.metainfo.xml) is maintained manually for Flathub — not overwritten from CMake.
44
#
55
# Architecture: x86_64 (build verification) — Flathub itself builds both x86_64 + aarch64
6-
# Output: Flatpak bundle published to Dropbox (rolling + versioned) until Flathub listing is live.
6+
# Output: Flatpak bundle published to Dropbox (rolling + versioned) for sideload/testing.
7+
# Users on Steam Deck should install from Flathub for automatic updates via Discover.
78
#
89
# Manifest is kept in sync with:
9-
# https://github.com/ForWard-Technologies-LLC/flathub/blob/add-pylux/io.github.ForWard_Technologies_LLC.Pylux.yml
10+
# https://github.com/flathub/io.github.ForWard_Technologies_LLC.Pylux
1011

1112
name: Deploy Flatpak (Flathub)
1213

@@ -130,8 +131,8 @@ jobs:
130131
if [ -n "${DROPBOX_URL}" ]; then
131132
INSTALL="curl -fsSL -o ${ROLLING_NAME} \"${DROPBOX_URL}\" && flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo && flatpak install --user -y ${ROLLING_NAME}"
132133
RUN="flatpak run io.github.ForWard_Technologies_LLC.Pylux"
133-
echo "### Install (until Flathub is approved)"
134-
echo "Download and install from the local file (Flatpak cannot install directly from a Dropbox URL):"
134+
echo "### Install (Dropbox bundle — sideload)"
135+
echo "Prefer Flathub: \`flatpak install flathub io.github.ForWard_Technologies_LLC.Pylux\`. To sideload this CI bundle:"
135136
echo ""
136137
echo '```bash'
137138
printf '%s\n' "$INSTALL"
@@ -144,21 +145,21 @@ jobs:
144145
echo '```'
145146
echo ""
146147
echo "### Update an existing Dropbox install"
147-
echo "Run the install command again when a new bundle is published. There is no \`flatpak update\` channel until the app is on Flathub."
148+
echo "Re-run the install command to update a sideloaded bundle. Flathub installs update with \`flatpak update\`."
148149
else
149150
echo "### Install"
150151
echo "Dropbox publish did not succeed — no install commands. Fix the publish step above."
151152
fi
152153
echo ""
153-
echo "### Once on Flathub"
154-
echo "\`flatpak install flathub io.github.ForWard_Technologies_LLC.Pylux\` — then \`flatpak update\` works normally."
154+
echo "### Flathub (recommended)"
155+
echo "\`flatpak install flathub io.github.ForWard_Technologies_LLC.Pylux\` — [store listing](https://flathub.org/apps/io.github.ForWard_Technologies_LLC.Pylux)"
155156
else
156157
echo "Flatpak build step outcome: **${{ steps.flatpak_build.outcome }}**. Fix manifest or build errors above."
157158
fi
158159
} >> "$GITHUB_STEP_SUMMARY"
159160
160-
# TODO: Uncomment after Flathub submission is approved and flathub/io.github.ForWard_Technologies_LLC.Pylux exists.
161-
# Also add a FLATHUB_TOKEN secret (PAT with write access to that repo).
161+
# Optional: uncomment to auto-sync manifest commit to flathub/io.github.ForWard_Technologies_LLC.Pylux on each deploy.
162+
# Requires FLATHUB_TOKEN secret (PAT with write access to that repo).
162163
#
163164
# sync-to-flathub:
164165
# name: Sync manifest to Flathub repo

.github/workflows/release-all.yml

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,26 +166,25 @@ jobs:
166166
FP_INSTALL=""
167167
fi
168168
FP_RUN="${FLATPAK_RUN:-flatpak run io.github.ForWard_Technologies_LLC.Pylux}"
169-
echo "- **Flatpak (interim, until Flathub is live)** — install:"
169+
echo "### Flatpak (Flathub — recommended for Steam Deck)"
170+
echo ""
171+
echo "**Get it:** [Flathub — io.github.ForWard_Technologies_LLC.Pylux](${FLATHUB_URL})"
172+
echo ""
173+
echo "- Install: \`flatpak install flathub io.github.ForWard_Technologies_LLC.Pylux\`"
174+
echo "- Run: \`${FP_RUN}\`"
175+
echo "- Update: \`flatpak update\` (or via Discover on Steam Deck)"
176+
echo "- **CI verify build succeeded:** ${FLATPAK_OK}"
177+
echo ""
178+
echo "**Optional — Dropbox sideload bundle** (same build, no Flathub update channel):"
170179
echo ' ```bash'
171180
if [ -n "$FP_INSTALL" ]; then
172181
printf ' %s\n' "$FP_INSTALL"
173182
else
174183
echo " (Flatpak install command unavailable — run Deploy Flatpak workflow first)"
175184
fi
176185
echo ' ```'
177-
echo " Run:"
178-
echo ' ```bash'
179-
printf ' %s\n' "$FP_RUN"
180-
echo ' ```'
181-
echo " To update: run the install command again when a new bundle is published (no \`flatpak update\` until Flathub listing)."
182-
echo ""
183-
184-
echo "### Flatpak (Flathub)"
186+
echo " Re-run the install command when a new bundle is published to update a sideloaded install."
185187
echo ""
186-
echo "**When approved:** [Flathub — io.github.ForWard_Technologies_LLC.Pylux](${FLATHUB_URL}) — \`flatpak install flathub io.github.ForWard_Technologies_LLC.Pylux\`"
187-
echo ""
188-
echo "- **CI verify build succeeded:** ${FLATPAK_OK}"
189188
} > "$NOTES_FILE"
190189
191190
gh release create "$TAG" --repo "$REPO" --title "Pylux ${TAG}" --notes-file "$NOTES_FILE"

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ tri_option(CHIAKI_USE_SYSTEM_CURL "Use system-provided curl instead of submodule
7474
# CI injects real values from the CHIAKI_VERSION_* lines below at archive time (.github/workflows/deploy-ios.yml).
7575
set(CHIAKI_VERSION_MAJOR 2)
7676
set(CHIAKI_VERSION_MINOR 10)
77-
set(CHIAKI_VERSION_PATCH 19)
77+
set(CHIAKI_VERSION_PATCH 21)
7878
set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH})
7979

8080
configure_file(

README.md

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](https://github.com/ForWard-Technologies-LLC/Pylux/blob/master/LICENSES/AGPL-3.0-only-OpenSSL.txt)
77
[![Platforms](https://img.shields.io/badge/platforms-Android%20%7C%20iOS%20%7C%20macOS%20%7C%20Windows%20%7C%20Linux-brightgreen)](https://github.com/ForWard-Technologies-LLC/Pylux/releases)
8+
[![Reddit: r/Pylux](https://img.shields.io/badge/Reddit-r%2FPylux-FF4500?logo=reddit&logoColor=white)](https://www.reddit.com/r/Pylux/)
89

9-
**Pylux is a free, open-source PS4 and PS5 Remote Play client for Android, Android TV, iOS, macOS, Windows, Linux, and Steam Deck.** It focuses on app-store installs, Internet Play (streaming the game catalog or your owned games), automatic console discovery, and a touch-friendly mobile UI — all from one community-maintained codebase.
10+
**Pylux is a free, open-source, community build PS4 and PS5 Remote Play client for Android, Android TV, iOS, macOS, Windows, Linux, and Steam Deck.** It focuses on app-store installs, Internet Play (streaming the game catalog or your owned games), automatic console discovery, and a touch-friendly mobile UI — all from one community-maintained codebase.
1011

1112
## Download
1213

@@ -16,19 +17,6 @@
1617

1718
<a href="https://flathub.org/apps/io.github.ForWard_Technologies_LLC.Pylux"><img src="assets/flathub-badge.svg" height="50" alt="Get it on Flathub"></a>&nbsp;<a href="https://www.dropbox.com/scl/fi/wi8bjilwiklv7fde0b4ea/pylux-latest.AppImage?rlkey=3xne4ltuiq54ogfmng4gq24mp&dl=1"><img src="assets/linux-appimage-badge.svg" height="50" alt="Download AppImage"></a>
1819

19-
**Steam Deck / Flathub Note**:
20-
21-
Pylux is not available in the Steam Deck Discover store yet. Flathub’s review process is slow, so until the listing is approved you need to install it manually using the commands below. I will remove this section once Pylux is available on Flathub.
22-
23-
Install:
24-
```
25-
curl -fsSL -o pylux-latest.flatpak "https://www.dropbox.com/scl/fi/zho2yrnso8u28rbx0jkwt/pylux-latest.flatpak?rlkey=kjftxhac24g43li6vpouqding&dl=1" && flatpak install --user -y pylux-latest.flatpak
26-
```
27-
Run:
28-
```
29-
flatpak run io.github.ForWard_Technologies_LLC.Pylux
30-
```
31-
3220
For full release notes and all downloads see the [Releases page](https://github.com/ForWard-Technologies-LLC/Pylux/releases).
3321

3422
## Screenshots
@@ -45,7 +33,7 @@ For full release notes and all downloads see the [Releases page](https://github.
4533
- **Internet Play** — stream games from the game catalog or your owned game library
4634
- **Remote Play** — low-latency streaming of your PlayStation console to any supported device
4735
- **Cross-platform** — Android, Android TV, iOS, iPadOS, macOS, Windows, Linux, Steam Deck
48-
- **App-store installs** — available on Google Play, App Store, and Mac App Store
36+
- **App-store installs** — available on Google Play, App Store, Mac App Store, and [Flathub](https://flathub.org/apps/io.github.ForWard_Technologies_LLC.Pylux) (Linux / Steam Deck)
4937
- **Automatic console discovery and registration**
5038
- **Touch-friendly controls** — mobile-optimized UI for phones and tablets
5139

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
2+
3+
package com.metallic.chiaki.cloudplay
4+
5+
object CloudLocale
6+
{
7+
const val DEFAULT = "en-US"
8+
9+
fun toImagicLocale(stored: String): String = stored.lowercase()
10+
11+
fun parseStorePath(stored: String): Pair<String, String>
12+
{
13+
val parts = stored.split("-", limit = 2)
14+
val language = parts.getOrNull(0)?.lowercase()?.takeIf { it.isNotEmpty() } ?: "en"
15+
val country = parts.getOrNull(1)?.uppercase()?.takeIf { it.isNotEmpty() } ?: "US"
16+
return country to language
17+
}
18+
19+
fun fromSession(language: String?, country: String?): String?
20+
{
21+
val lang = language?.trim().orEmpty()
22+
val cty = country?.trim().orEmpty()
23+
if (lang.isEmpty() || cty.isEmpty())
24+
return null
25+
return "$lang-${cty.uppercase()}"
26+
}
27+
28+
/** Non-fatal warning when locale could not be learned from Kamaji (catalog may use en-US). */
29+
fun unconfiguredWarning(): String =
30+
"Could not detect your PlayStation region. The catalog may not match your store."
31+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
2+
3+
package com.metallic.chiaki.cloudplay
4+
5+
import android.util.Log
6+
import com.metallic.chiaki.cloudplay.api.HttpClient
7+
import com.metallic.chiaki.common.Preferences
8+
import org.json.JSONObject
9+
10+
object CloudLocaleBootstrap
11+
{
12+
private const val TAG = "CloudLocaleBootstrap"
13+
private val lock = Any()
14+
15+
fun ensureConfigured(preferences: Preferences, npssoToken: String): Boolean
16+
{
17+
if (preferences.isCloudLanguageConfigured())
18+
return true
19+
if (npssoToken.isBlank())
20+
{
21+
Log.w(TAG, "Cannot bootstrap locale: empty npsso token")
22+
return false
23+
}
24+
25+
synchronized(lock)
26+
{
27+
if (preferences.isCloudLanguageConfigured())
28+
return true
29+
30+
Log.i(TAG, "Bootstrapping cloud locale via Kamaji session (first time only)")
31+
return runBootstrap(preferences, npssoToken)
32+
}
33+
}
34+
35+
private fun runBootstrap(preferences: Preferences, npssoToken: String): Boolean
36+
{
37+
return try
38+
{
39+
val duid = DuidUtil.generateDuid()
40+
val oauthCode = fetchOAuthCode(npssoToken, duid) ?: run {
41+
Log.w(TAG, "Locale bootstrap failed: OAuth")
42+
return false
43+
}
44+
if (!createKamajiSessionAndSaveLocale(preferences, oauthCode, duid))
45+
{
46+
Log.w(TAG, "Locale bootstrap failed: Kamaji session")
47+
return false
48+
}
49+
Log.i(TAG, "Locale bootstrap OK: ${preferences.getCloudLanguage()}")
50+
true
51+
}
52+
catch (e: Exception)
53+
{
54+
Log.w(TAG, "Locale bootstrap error", e)
55+
false
56+
}
57+
}
58+
59+
private fun fetchOAuthCode(npssoToken: String, duid: String): String?
60+
{
61+
val uri = android.net.Uri.parse("${PsnApiConstants.ACCOUNT_BASE}/v1/oauth/authorize")
62+
.buildUpon()
63+
.appendQueryParameter("smcid", "pc:psnow")
64+
.appendQueryParameter("applicationId", "psnow")
65+
.appendQueryParameter("response_type", "code")
66+
.appendQueryParameter("scope", PsnApiConstants.PS4_SCOPES)
67+
.appendQueryParameter("client_id", PsnApiConstants.CLIENT_ID)
68+
.appendQueryParameter("redirect_uri", PsnApiConstants.REDIRECT_URI)
69+
.appendQueryParameter("service_entity", "urn:service-entity:psn")
70+
.appendQueryParameter("prompt", "none")
71+
.appendQueryParameter("renderMode", "mobilePortrait")
72+
.appendQueryParameter("hidePageElements", "forgotPasswordLink")
73+
.appendQueryParameter("displayFooter", "none")
74+
.appendQueryParameter("disableLinks", "qriocityLink")
75+
.appendQueryParameter("mid", "PSNOW")
76+
.appendQueryParameter("duid", duid)
77+
.appendQueryParameter("layout_type", "popup")
78+
.appendQueryParameter("service_logo", "ps")
79+
.appendQueryParameter("tp_psn", "true")
80+
.appendQueryParameter("noEVBlock", "true")
81+
.build()
82+
83+
val response = HttpClient.get(uri.toString(), mapOf("Cookie" to "npsso=$npssoToken"), followRedirects = false)
84+
if (response.statusCode != 302)
85+
return null
86+
87+
val location = HttpClient.extractLocation(response.headers) ?: return null
88+
val match = Regex("[?&]code=([^&]+)").find(location) ?: return null
89+
return match.groupValues.getOrNull(1)?.takeIf { it.isNotEmpty() }
90+
}
91+
92+
private fun createKamajiSessionAndSaveLocale(
93+
preferences: Preferences,
94+
oauthCode: String,
95+
duid: String
96+
): Boolean
97+
{
98+
val url = "${PsnApiConstants.KAMAJI_BASE}/user/session"
99+
val body = "code=$oauthCode&client_id=${PsnApiConstants.CLIENT_ID}&duid=$duid"
100+
val headers = mapOf(
101+
"Content-Type" to "text/plain;charset=UTF-8",
102+
"X-Alt-Referer" to PsnApiConstants.REDIRECT_URI,
103+
"Origin" to PsnApiConstants.ORIGIN,
104+
"Referer" to PsnApiConstants.REFERER,
105+
"Accept" to "*/*"
106+
)
107+
108+
val response = HttpClient.post(url, body, headers)
109+
if (response.statusCode != 200)
110+
return false
111+
112+
val json = JSONObject(response.body)
113+
if (json.optJSONObject("header")?.optString("status_code") != "0x0000")
114+
return false
115+
116+
val data = json.optJSONObject("data")
117+
preferences.setCloudLanguageFromSession(
118+
data?.optString("language"),
119+
data?.optString("country")
120+
)
121+
return preferences.isCloudLanguageConfigured()
122+
}
123+
}

android/app/src/main/java/com/metallic/chiaki/cloudplay/api/CloudStreamingBackend.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package com.metallic.chiaki.cloudplay.api
44

55
import android.content.Context
66
import android.util.Log
7+
import com.metallic.chiaki.cloudplay.CloudLocaleBootstrap
78
import com.metallic.chiaki.cloudplay.DuidUtil
89
import com.metallic.chiaki.cloudplay.PsnApiConstants
910
import com.metallic.chiaki.cloudplay.model.CloudStreamSession
@@ -95,6 +96,10 @@ class CloudStreamingBackend(
9596
}
9697

9798
Log.i(TAG, "✓ Authorization check passed")
99+
100+
// PSCloud skips Kamaji; bootstrap locale once if PSNow never ran
101+
if (normalizedServiceType == "pscloud")
102+
CloudLocaleBootstrap.ensureConfigured(preferences, npssoToken)
98103

99104
// Continue with cloud session setup
100105
val result = continueCloudSessionAfterAuth(

android/app/src/main/java/com/metallic/chiaki/cloudplay/api/PSGaikaiStreaming.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,13 +1189,17 @@ catch (e: Exception)
11891189
body.put("dataCenter", selectedDatacenter)
11901190

11911191
// Network info from ping results
1192+
val cloudBwKbps = if (serviceType == "pscloud")
1193+
preferences.getCloudBitratePscloud()
1194+
else
1195+
preferences.getCloudBitratePsnow()
11921196
val network = JSONObject()
1193-
network.put("bwKbpsSent", 50000) // 50 Mbps upload
1197+
network.put("bwKbpsSent", cloudBwKbps)
11941198
network.put("bwLoss", 0.001) // 0.1% packet loss
11951199
network.put("mtu", selectedDatacenterPingResult.optInt("mtu_in", 1454))
11961200
network.put("rtt", selectedDatacenterPingResult.optInt("rtt", 25))
11971201
network.put("port", selectedDatacenterPort)
1198-
network.put("bwKbpsReceived", 200000) // 200 Mbps download
1202+
network.put("bwKbpsReceived", cloudBwKbps)
11991203
network.put("bwLossUpstream", 0)
12001204
network.put("mtuUpstream", selectedDatacenterPingResult.optInt("mtu_out", 1254))
12011205
body.put("network", network)

android/app/src/main/java/com/metallic/chiaki/cloudplay/api/PSKamajiSession.kt

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,8 @@ class PSKamajiSession(
279279

280280
if (!sessionCountry.isNullOrEmpty() && !sessionLanguage.isNullOrEmpty())
281281
{
282-
// Format: language-COUNTRY (e.g., "nl-NL" or "en-US")
283-
val locale = "$sessionLanguage-${sessionCountry.uppercase()}"
284-
preferences.setCloudLanguage(locale)
285-
Log.i(TAG, "Saved locale from session: $locale")
282+
preferences.setCloudLanguageFromSession(sessionLanguage, sessionCountry)
283+
Log.i(TAG, "Saved locale from session: ${preferences.getCloudLanguage()}")
286284
}
287285
}
288286
}
@@ -310,18 +308,9 @@ class PSKamajiSession(
310308
{
311309
try
312310
{
313-
// Get locale from unified language setting (Qt line 321: GetCloudLanguagePSCloud)
314-
// Qt uses ONE setting for both PSNow and PSCloud
315-
val localeSetting = preferences.getCloudLanguage() // Default "en-US"
316-
val locale = localeSetting.lowercase() // Convert "en-US" to "en-us"
317-
318-
// Extract country and language from locale (e.g., "en-us" -> "US", "en")
319-
val localeParts = locale.split("-")
320-
val country = if (localeParts.size > 1) localeParts[1].uppercase() else "US"
321-
val language = if (localeParts.isNotEmpty()) localeParts[0].lowercase() else "en"
322-
311+
val localeSetting = preferences.getCloudLanguage()
312+
val (country, language) = com.metallic.chiaki.cloudplay.CloudLocale.parseStorePath(localeSetting)
323313
Log.i(TAG, "Using locale from settings: $localeSetting -> country=$country, language=$language")
324-
325314
val url = "$storeBase/container/$country/$language/19/$productId?useOffers=true&gkb=1&gkb2=1"
326315

327316
Log.d(TAG, "Step 0.5d: Convert Product ID")

0 commit comments

Comments
 (0)