diff --git a/AGENTS.md b/AGENTS.md
index 816d9590..9a0999af 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -26,6 +26,15 @@
- Never call `HttpClient(...)` directly. Inject `HttpClientFactory` (`commonMain/.../core/network/HttpClientFactory.kt`, a Koin single) and use `.default` / `.json` (shared, never close them) or `.create { ... }` for extra plugins (caller owns and may close it). It owns the one engine-backed client, so the platform default engine is used everywhere (Darwin on iOS, OkHttp on Android/desktop).
- Do not add `ktor-client-cio`: on iOS it competes with Darwin as the default engine and has no TLS on Kotlin/Native, so every HTTPS request fails.
+## Group Jam
+- Room server: separate repository, https://github.com/team-spotube/jam-server (Cloudflare Worker + one Durable Object per six-letter room code, TypeScript, wrangler). It relays/retains small JSON frames and serves the clock; no audio passes through it. Protocol in its `src/protocol.ts`; keep it in sync with the wire frames in `core/jam/JamRoomClient.kt` (bump `PROTOCOL_VERSION` on both sides for breaking changes, and land both changes together).
+- App side: `core/jam/` — `JamRoomClient` (Ktor WebSocket, reconnects, keep-alives, clock pings), `JamClock`/`JamDriftCorrector` (sync math), `JamRoomService` (host broadcasts queue + playback anchors in server time; guests follow by nudging playback rate, seeking only when far off).
+- Cost and battery: the room is billed per incoming message and storage write, and on phones each transmission wakes the cellular radio. Don't add periodic frames or polling. Keep-alives go out once a minute. Guests' sync loop sleeps while the room is paused or buffering, and the host checks its own drift only while playing. Anchors go out on change (plus host drift and a 60 s heartbeat), keep-alives are runtime auto-responses, and anchors aren't persisted. See the jam-server README.
+- Security: the server enforces identity (per-install secret → per-room key → public id), device-level bans (`JamDevice.kt` traits, only ever sent hashed), host-only powers and flood limits. The app treats everything from peers as untrusted and runs it through `JamMediaItem.sanitized` before loading it. See "Security" in the jam-server README.
+- A network drop must never crash the app: `JamRoomClient.pump` turns socket failures into a reconnect. `JamSessionSimulationTest.roomSurvivesNetworkLoss` cuts the network with `TcpProxy`.
+- Server URL setting: `UserSettings.jamServerUrl` (default `DEFAULT_JAM_SERVER_URL` in `SettingsModels.kt`).
+- Tests: `JamSyncTest` (pure), plus `JamRoomClientIntegrationTest` and `JamSessionSimulationTest`, which only run with `JAM_TEST_URL` set (run `npm run dev` in a jam-server checkout).
+
## GraalVM native image (desktop)
- Enabled via `graalvm { }` inside `nucleus.application` in `composeApp/build.gradle.kts`. Nucleus downloads GraalVM CE itself (or uses `$GRAALVM_HOME`). Linux needs `gcc` + `patchelf`.
- Build: `:composeApp:createGraalvmNativeDistributable` (output `composeApp/build/compose/binaries/main/graalvm-app/`), installers via `packageGraalvm{Deb,Rpm,AppImage,Dmg,Msi,Exe}`. A build takes ~10 min and uses all cores and ~15 GB RAM.
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index f60ba709..38967b31 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -289,12 +289,6 @@ kotlin {
// DLNA
implementation(libs.dns.sd.kt)
- // mqtt client for jam-session
- implementation(libs.mqtt.client)
- implementation(libs.mqtt.x.models)
- implementation(libs.mqtt.buffer)
- implementation(libs.mqtt.buffer.codec)
-
// Markdown renderer
implementation(libs.multiplatform.markdown.renderer)
implementation(libs.multiplatform.markdown.renderer.m3)
@@ -443,14 +437,6 @@ android {
}
-// ditchoom's socket-quic-quiche ships libquiche_jni.so with 4 KB LOAD segment alignment
-// (not 16 KB compatible), even in the latest release. It is only an optional QUIC transport
-// pulled in transitively by mqtt-client, so drop it on Android until upstream fixes it.
-configurations.matching { it.name.startsWith("debug") || it.name.startsWith("release") }.configureEach {
- exclude(group = "com.ditchoom", module = "socket-quic-quiche")
- exclude(group = "com.ditchoom", module = "socket-quic-quiche-android")
-}
-
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
debugImplementation(libs.compose.uiTooling)
diff --git a/composeApp/src/androidMain/kotlin/org/spotube/spotube/core/jam/JamDevice.android.kt b/composeApp/src/androidMain/kotlin/org/spotube/spotube/core/jam/JamDevice.android.kt
new file mode 100644
index 00000000..45d18513
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/org/spotube/spotube/core/jam/JamDevice.android.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
+ *
+ * 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 .
+ */
+
+package org.spotube.spotube.core.jam
+
+import android.content.res.Resources
+import android.os.Build
+import java.util.Locale
+import java.util.TimeZone
+
+internal actual fun jamDeviceTraits(): List {
+ val display = Resources.getSystem().displayMetrics
+ return listOf(
+ "android",
+ Build.MANUFACTURER,
+ Build.MODEL,
+ Build.DEVICE,
+ Build.VERSION.RELEASE,
+ Build.VERSION.SDK_INT.toString(),
+ "${minOf(display.widthPixels, display.heightPixels)}x${maxOf(display.widthPixels, display.heightPixels)}@${display.densityDpi}",
+ Locale.getDefault().toLanguageTag(),
+ TimeZone.getDefault().id)
+}
diff --git a/composeApp/src/androidMain/kotlin/org/spotube/spotube/media/PlaybackService.kt b/composeApp/src/androidMain/kotlin/org/spotube/spotube/media/PlaybackService.kt
index 74df39e7..26f5a071 100644
--- a/composeApp/src/androidMain/kotlin/org/spotube/spotube/media/PlaybackService.kt
+++ b/composeApp/src/androidMain/kotlin/org/spotube/spotube/media/PlaybackService.kt
@@ -165,7 +165,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
librarySession.release()
super.onDestroy()
// Swiping the app out of recents should fully close it: Android otherwise keeps this
- // process cached with the proxy server, MQTT client and every singleton still alive.
+ // process cached with the proxy server, jam client and every singleton still alive.
if (taskRemoved) Process.killProcess(Process.myPid())
}
diff --git a/composeApp/src/commonMain/composeResources/values-ar/strings.xml b/composeApp/src/commonMain/composeResources/values-ar/strings.xml
index 9eb21d8c..cc82d900 100644
--- a/composeApp/src/commonMain/composeResources/values-ar/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ar/strings.xml
@@ -308,25 +308,11 @@
WebView
عرض محتوى الويب داخل التطبيق
الجام الجماعي
- وسيط MQTT
- مضيف الوسيط
- %1$s:%2$d
- منفذ الوسيط
- استخدام TLS
- اسم المستخدم (اختياري)
- كلمة المرور (اختياري)
- بادئة معرّف العميل
- تُستخدم لتعريف نسخة Spotube هذه
- تشفير الاتصال بالوسيط
- تحقق من إعدادات الوسيط الحالية قبل بدء غرفة.
- اختبار الاتصال
- جارٍ الاختبار…
- تم الاتصال خلال %1$d ms
- فشل: %1$s
- وسيط مؤقت — اضبط وسيطك الخاص للاستضافة الذاتية
+ اختبار الاتصال
+ جارٍ الاختبار…
+ تم الاتصال خلال %1$d ms
+ فشل: %1$s
الاتصال
- بيانات الاعتماد
- الهوية
التشخيص
@@ -339,9 +325,6 @@
جارٍ الاتصال…
رمز الغرفة
ABC123
- الوسيط: %1$s
- اضبط وسيطًا في الإعدادات لاستضافة غرفة أو الانضمام إليها.
- إعدادات الوسيط
متصل
جارٍ إعادة الاتصال…
نسخ الرمز
@@ -361,6 +344,28 @@
تعذّر الانضمام إلى جلسة الجام
أدخل رمز غرفة مكوّنًا من ستة أحرف وأرقام.
انضم إلى جام Spotube الخاص بي باستخدام الرمز: %1$s
+ خادم الغرف
+ رابط الخادم
+ أدخل رابطًا يبدأ بـ https://
+ تحقّق من خادم الغرف قبل بدء غرفة.
+ يستخدم الجام خادم الغرف المجاني من Spotube. للاستضافة الذاتية، انشر الخادم من github.com/team-spotube/jam-server وأدخل رابطه.
+ أنت
+ الخادم: %1$s
+ حدّد خادم غرف في الإعدادات لاستضافة غرفة أو الانضمام إليها.
+ إعدادات الخادم
+ %1$s (أنت)
+ اسمك
+ يراك الآخرون في الجام باسم %1$s
+ يظهر لجميع من في الجام. اتركه فارغًا لاستخدام %1$s. يُطبَّق الاسم الجديد في المرة القادمة التي تنشئ فيها غرفة أو تنضم إليها.
+ استخدم %1$d حرفًا كحد أقصى.
+ لم يتم إعداد خادم للجام
+ تعذّر الوصول إلى خادم الجام. تحقّق من الشبكة أو من رابط الخادم في الإعدادات ← الجام الجماعي.
+ يعمل خادم الجام بإصدار غير متوافق.
+ لا توجد غرفة بهذا الرمز. تحقّق منه وحاول مجددًا.
+ هذه الغرفة ممتلئة.
+ حظرك المضيف من هذه الغرفة.
+ أزالك المضيف من الغرفة.
+ فُتحت هذه الغرفة مجددًا على هذا الجهاز، لذا أُغلق هذا الاتصال.
الانتقال إلى الألبوم
مؤقت النوم
@@ -533,14 +538,9 @@
اقتراح للجام
- لم يتم ضبط وسيط للجام
- مضيف الوسيط فارغ
فشل الاتصال
انقطع الاتصال
-
- الوسيط
- مجهول
تمت الإضافة إلى قائمة انتظار الجام
diff --git a/composeApp/src/commonMain/composeResources/values-bn/strings.xml b/composeApp/src/commonMain/composeResources/values-bn/strings.xml
index 268cdf95..a111017f 100644
--- a/composeApp/src/commonMain/composeResources/values-bn/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-bn/strings.xml
@@ -292,25 +292,11 @@
WebView
অ্যাপের ভেতরে ওয়েব কনটেন্ট দেখানো
গ্রুপ জ্যাম
- MQTT ব্রোকার
- ব্রোকার হোস্ট
- %1$s:%2$d
- ব্রোকার পোর্ট
- TLS ব্যবহার করুন
- ইউজারনেম (ঐচ্ছিক)
- পাসওয়ার্ড (ঐচ্ছিক)
- ক্লায়েন্ট ID প্রিফিক্স
- এই Spotube শনাক্ত করতে ব্যবহৃত হয়
- ব্রোকার কানেকশন এনক্রিপ্ট করুন
- রুম শুরু করার আগে বর্তমান ব্রোকার সেটিংস যাচাই করুন।
- কানেকশন পরীক্ষা করুন
- পরীক্ষা চলছে…
- %1$d ms-এ কানেক্ট হয়েছে
- ব্যর্থ: %1$s
- প্লেসহোল্ডার ব্রোকার — সেলফ-হোস্ট করতে নিজেরটি কনফিগার করুন
+ কানেকশন পরীক্ষা করুন
+ পরীক্ষা চলছে…
+ %1$d ms-এ কানেক্ট হয়েছে
+ ব্যর্থ: %1$s
কানেকশন
- ক্রেডেনশিয়াল
- পরিচিতি
ডায়াগনস্টিকস
@@ -323,9 +309,6 @@
কানেক্ট হচ্ছে…
রুম কোড
ABC123
- ব্রোকার: %1$s
- রুম হোস্ট করতে বা যোগ দিতে সেটিংসে একটি ব্রোকার সেট আপ করুন।
- ব্রোকার সেটিংস
কানেক্টেড
আবার কানেক্ট হচ্ছে…
কোড কপি করুন
@@ -345,6 +328,28 @@
জ্যাম সেশনে যোগ দেওয়া যায়নি
অক্ষর ও সংখ্যা দিয়ে ছয় অক্ষরের একটি রুম কোড দিন।
এই কোড দিয়ে আমার Spotube জ্যামে যোগ দিন: %1$s
+ রুম সার্ভার
+ সার্ভার URL
+ https:// দিয়ে শুরু হওয়া একটি URL দিন
+ রুম শুরু করার আগে রুম সার্ভার পরীক্ষা করুন।
+ জ্যাম Spotube-এর বিনামূল্যের রুম সার্ভার ব্যবহার করে। নিজে হোস্ট করতে github.com/team-spotube/jam-server থেকে সার্ভার ডিপ্লয় করে তার URL দিন।
+ আপনি
+ সার্ভার: %1$s
+ রুম হোস্ট করতে বা যোগ দিতে সেটিংসে একটি রুম সার্ভার সেট করুন।
+ সার্ভার সেটিংস
+ %1$s (আপনি)
+ আপনার নাম
+ জ্যামের অন্যরা আপনাকে %1$s নামে দেখবে
+ জ্যামের সবাই দেখতে পাবে। %1$s ব্যবহার করতে ফাঁকা রাখুন। নতুন নাম পরের বার রুম তৈরি বা যোগ দেওয়ার সময় থেকে কার্যকর হবে।
+ সর্বোচ্চ %1$dটি অক্ষর ব্যবহার করুন।
+ কোনো জ্যাম সার্ভার সেট করা নেই
+ জ্যাম সার্ভারে পৌঁছানো যায়নি। আপনার নেটওয়ার্ক বা সেটিংস → গ্রুপ জ্যাম-এ সার্ভার URL পরীক্ষা করুন।
+ জ্যাম সার্ভারের সংস্করণটি সামঞ্জস্যপূর্ণ নয়।
+ এই কোডের কোনো রুম নেই। কোডটি দেখে আবার চেষ্টা করুন।
+ এই রুমটি পূর্ণ।
+ হোস্ট আপনাকে এই রুম থেকে ব্যান করেছেন।
+ হোস্ট আপনাকে রুম থেকে সরিয়ে দিয়েছেন।
+ এই রুমটি এই ডিভাইসে আবার খোলা হয়েছে, তাই এই কানেকশনটি বন্ধ করা হয়েছে।
অ্যালবামে যান
স্লিপ টাইমার
@@ -517,14 +522,9 @@
জ্যামে সাজেস্ট করুন
- কোনো জ্যাম ব্রোকার কনফিগার করা নেই
- ব্রোকার হোস্ট ফাঁকা
কানেকশন ব্যর্থ হয়েছে
কানেকশন বিচ্ছিন্ন হয়েছে
-
- ব্রোকার
- anonymous
জ্যাম কিউতে যোগ করা হয়েছে
diff --git a/composeApp/src/commonMain/composeResources/values-de/strings.xml b/composeApp/src/commonMain/composeResources/values-de/strings.xml
index 2f965807..3fb3063b 100644
--- a/composeApp/src/commonMain/composeResources/values-de/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-de/strings.xml
@@ -292,25 +292,11 @@
WebView
Webinhalte in der App anzeigen
Gruppen-Jam
- MQTT-Broker
- Broker-Host
- %1$s:%2$d
- Broker-Port
- TLS verwenden
- Benutzername (optional)
- Passwort (optional)
- Client-ID-Präfix
- Dient zur Identifizierung dieser Spotube-Instanz
- Verbindung zum Broker verschlüsseln
- Prüfe die aktuellen Broker-Einstellungen, bevor du einen Raum startest.
- Verbindung testen
- Wird getestet…
- Verbunden in %1$d ms
- Fehlgeschlagen: %1$s
- Platzhalter-Broker – richte deinen eigenen ein, um selbst zu hosten
+ Verbindung testen
+ Wird getestet…
+ Verbunden in %1$d ms
+ Fehlgeschlagen: %1$s
Verbindung
- Zugangsdaten
- Identität
Diagnose
@@ -323,9 +309,6 @@
Verbinden…
Raumcode
ABC123
- Broker: %1$s
- Richte in den Einstellungen einen Broker ein, um einen Raum zu hosten oder beizutreten.
- Broker-Einstellungen
Verbunden
Verbindung wird wiederhergestellt…
Code kopieren
@@ -345,6 +328,28 @@
Beitritt zur Jam-Session fehlgeschlagen
Gib einen sechsstelligen Raumcode aus Buchstaben und Ziffern ein.
Tritt meiner Spotube-Jam bei, Code: %1$s
+ Raumserver
+ Server-URL
+ Gib eine URL ein, die mit https:// beginnt.
+ Prüfe den Raumserver, bevor du einen Raum startest.
+ Jam nutzt den kostenlosen Raumserver von Spotube. Zum Selbsthosten stelle den Server von github.com/team-spotube/jam-server bereit und gib seine URL ein.
+ Du
+ Server: %1$s
+ Lege in den Einstellungen einen Raumserver fest, um einen Raum zu hosten oder beizutreten.
+ Servereinstellungen
+ %1$s (du)
+ Dein Name
+ Andere im Jam sehen dich als %1$s
+ Wird allen im Jam angezeigt. Lass das Feld leer, um %1$s zu verwenden. Ein neuer Name gilt ab dem nächsten Raum, den du startest oder dem du beitrittst.
+ Verwende höchstens %1$d Zeichen.
+ Kein Jam-Server eingerichtet
+ Der Jam-Server ist nicht erreichbar. Prüfe dein Netzwerk oder die Server-URL unter Einstellungen → Gruppen-Jam.
+ Der Jam-Server verwendet eine inkompatible Version.
+ Kein Raum mit diesem Code. Prüfe ihn und versuche es erneut.
+ Dieser Raum ist voll.
+ Der Host hat dich aus diesem Raum gesperrt.
+ Der Host hat dich aus dem Raum entfernt.
+ Dieser Raum wurde auf diesem Gerät erneut geöffnet, daher wurde diese Verbindung getrennt.
Zum Album
Sleep-Timer
@@ -517,14 +522,9 @@
Für Jam vorschlagen
- Kein Jam-Broker konfiguriert
- Broker-Host ist leer
Verbindung fehlgeschlagen
Verbindung unterbrochen
-
- Broker
- anonym
Zur Jam-Warteschlange hinzugefügt
diff --git a/composeApp/src/commonMain/composeResources/values-es/strings.xml b/composeApp/src/commonMain/composeResources/values-es/strings.xml
index b4ae5fa1..3a79f26d 100644
--- a/composeApp/src/commonMain/composeResources/values-es/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-es/strings.xml
@@ -296,25 +296,11 @@
WebView
Mostrar contenido web dentro de la app
Jam en grupo
- Broker MQTT
- Host del broker
- %1$s:%2$d
- Puerto del broker
- Usar TLS
- Usuario (opcional)
- Contraseña (opcional)
- Prefijo de ID de cliente
- Se usa para identificar esta instancia de Spotube
- Cifra la conexión con el broker
- Comprueba los ajustes actuales del broker antes de crear una sala.
- Probar conexión
- Probando…
- Conectado en %1$d ms
- Error: %1$s
- Broker de ejemplo: configura el tuyo para alojarlo tú mismo
+ Probar conexión
+ Probando…
+ Conectado en %1$d ms
+ Error: %1$s
Conexión
- Credenciales
- Identidad
Diagnóstico
@@ -327,9 +313,6 @@
Conectando…
Código de la sala
ABC123
- Broker: %1$s
- Configura un broker en Ajustes para crear una sala o unirte a una.
- Ajustes del broker
Conectado
Reconectando…
Copiar código
@@ -349,6 +332,28 @@
No se pudo unir a la sesión de Jam
Introduce un código de sala de seis caracteres con letras y números.
Únete a mi Jam de Spotube con el código: %1$s
+ Servidor de salas
+ URL del servidor
+ Introduce una URL que empiece por https://
+ Comprueba el servidor de salas antes de crear una sala.
+ Jam usa el servidor de salas gratuito de Spotube. Para alojarlo tú mismo, despliega el servidor de github.com/team-spotube/jam-server e introduce su URL.
+ Tú
+ Servidor: %1$s
+ Configura un servidor de salas en Ajustes para crear una sala o unirte a una.
+ Ajustes del servidor
+ %1$s (tú)
+ Tu nombre
+ Los demás en la jam te ven como %1$s
+ Todos en la jam lo verán. Déjalo vacío para usar %1$s. Un nombre nuevo se aplica la próxima vez que crees una sala o te unas a una.
+ Usa como máximo %1$d caracteres.
+ No hay ningún servidor de jam configurado
+ No se pudo conectar con el servidor de jam. Revisa tu red o la URL del servidor en Ajustes → Jam en grupo.
+ El servidor de jam usa una versión incompatible.
+ No hay ninguna sala con ese código. Revísalo e inténtalo de nuevo.
+ Esta sala está llena.
+ El anfitrión te ha bloqueado en esta sala.
+ El anfitrión te ha quitado de la sala.
+ Esta sala se abrió de nuevo en este dispositivo, así que se cerró esta conexión.
Ir al álbum
Temporizador
@@ -521,14 +526,9 @@
Sugerir al Jam
- No hay ningún broker de Jam configurado
- El host del broker está vacío
Error de conexión
Se perdió la conexión
-
- Broker
- anónimo
Añadida a la cola del Jam
diff --git a/composeApp/src/commonMain/composeResources/values-fa/strings.xml b/composeApp/src/commonMain/composeResources/values-fa/strings.xml
index 51374e7e..7ad121f8 100644
--- a/composeApp/src/commonMain/composeResources/values-fa/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-fa/strings.xml
@@ -292,25 +292,11 @@
WebView
نمایش محتوای وب درون برنامه
جم گروهی
- بروکر MQTT
- میزبان بروکر
- %1$s:%2$d
- درگاه بروکر
- استفاده از TLS
- نام کاربری (اختیاری)
- گذرواژه (اختیاری)
- پیشوند شناسهٔ کلاینت
- برای شناسایی این نسخه از Spotube استفاده میشود
- رمزگذاری اتصال به بروکر
- پیش از شروع اتاق، تنظیمات فعلی بروکر را بررسی کنید.
- آزمایش اتصال
- در حال آزمایش…
- در %1$d میلیثانیه متصل شد
- ناموفق: %1$s
- بروکر نمونه — برای میزبانی شخصی، بروکر خودتان را پیکربندی کنید
+ آزمایش اتصال
+ در حال آزمایش…
+ در %1$d میلیثانیه متصل شد
+ ناموفق: %1$s
اتصال
- اطلاعات ورود
- هویت
عیبیابی
@@ -323,9 +309,6 @@
در حال اتصال…
کد اتاق
ABC123
- بروکر: %1$s
- برای میزبانی یا پیوستن به اتاق، یک بروکر در تنظیمات راهاندازی کنید.
- تنظیمات بروکر
متصل
در حال اتصال دوباره…
کپی کد
@@ -345,6 +328,28 @@
پیوستن به جلسهٔ جم ناموفق بود
یک کد اتاق ششنویسهای شامل حروف و اعداد وارد کنید.
با این کد به جم Spotube من بپیوندید: %1$s
+ سرور اتاقها
+ نشانی سرور
+ نشانیای وارد کنید که با https:// شروع شود
+ پیش از شروع اتاق، سرور اتاقها را بررسی کنید.
+ جم از سرور رایگان اتاقهای Spotube استفاده میکند. برای میزبانی شخصی، سرور را از github.com/team-spotube/jam-server راهاندازی کنید و نشانی آن را وارد کنید.
+ شما
+ سرور: %1$s
+ برای میزبانی یا پیوستن به اتاق، در تنظیمات یک سرور اتاق تعیین کنید.
+ تنظیمات سرور
+ %1$s (شما)
+ نام شما
+ دیگران در جم شما را با نام %1$s میبینند
+ به همه در جم نشان داده میشود. برای استفاده از %1$s خالی بگذارید. نام جدید از دفعهٔ بعدی که اتاقی میسازید یا به آن میپیوندید اعمال میشود.
+ حداکثر %1$d نویسه وارد کنید.
+ هیچ سرور جمی تنظیم نشده است
+ دسترسی به سرور جم ممکن نیست. شبکه یا نشانی سرور را در تنظیمات ← جم گروهی بررسی کنید.
+ سرور جم نسخهٔ ناسازگاری اجرا میکند.
+ اتاقی با این کد وجود ندارد. آن را بررسی کنید و دوباره امتحان کنید.
+ این اتاق پر است.
+ میزبان شما را از این اتاق مسدود کرد.
+ میزبان شما را از اتاق بیرون کرد.
+ این اتاق دوباره روی همین دستگاه باز شد، بنابراین این اتصال بسته شد.
رفتن به آلبوم
زمانسنج خواب
@@ -517,14 +522,9 @@
پیشنهاد به جم
- هیچ بروکر جمی پیکربندی نشده است
- میزبان بروکر خالی است
اتصال ناموفق بود
اتصال قطع شد
-
- بروکر
- ناشناس
به صف جم اضافه شد
diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml
index 45a31bd6..a96a3b83 100644
--- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml
@@ -296,25 +296,11 @@
WebView
Afficher du contenu web dans l\'application
Jam de groupe
- Broker MQTT
- Hôte du broker
- %1$s:%2$d
- Port du broker
- Utiliser TLS
- Nom d\'utilisateur (facultatif)
- Mot de passe (facultatif)
- Préfixe de l\'ID client
- Sert à identifier cette instance de Spotube
- Chiffrer la connexion au broker
- Vérifiez les paramètres actuels du broker avant de créer une salle.
- Tester la connexion
- Test en cours…
- Connecté en %1$d ms
- Échec : %1$s
- Broker temporaire : configurez le vôtre pour l\'auto-héberger
+ Tester la connexion
+ Test en cours…
+ Connecté en %1$d ms
+ Échec : %1$s
Connexion
- Identifiants
- Identité
Diagnostic
@@ -327,9 +313,6 @@
Connexion…
Code de la salle
ABC123
- Broker : %1$s
- Configurez un broker dans les Paramètres pour créer ou rejoindre une salle.
- Paramètres du broker
Connecté
Reconnexion…
Copier le code
@@ -349,6 +332,28 @@
Impossible de rejoindre la session Jam
Saisissez un code de salle de six caractères (lettres et chiffres).
Rejoignez ma Jam Spotube avec le code : %1$s
+ Serveur de salles
+ URL du serveur
+ Saisissez une URL commençant par https://
+ Vérifiez le serveur de salles avant de lancer une salle.
+ Jam utilise le serveur de salles gratuit de Spotube. Pour l’héberger vous-même, déployez le serveur depuis github.com/team-spotube/jam-server et saisissez son URL.
+ Vous
+ Serveur : %1$s
+ Définissez un serveur de salles dans les Paramètres pour créer ou rejoindre une salle.
+ Paramètres du serveur
+ %1$s (vous)
+ Votre nom
+ Les autres participants vous voient comme %1$s
+ Visible par tous les participants de la jam. Laissez vide pour utiliser %1$s. Un nouveau nom s’applique la prochaine fois que vous créez ou rejoignez une salle.
+ Utilisez au maximum %1$d caractères.
+ Aucun serveur de jam configuré
+ Impossible de joindre le serveur de jam. Vérifiez votre réseau ou l’URL du serveur dans Paramètres → Jam de groupe.
+ Le serveur de jam utilise une version incompatible.
+ Aucune salle avec ce code. Vérifiez-le et réessayez.
+ Cette salle est pleine.
+ L’hôte vous a banni de cette salle.
+ L’hôte vous a retiré de la salle.
+ Cette salle a été rouverte sur cet appareil, cette connexion a donc été fermée.
Accéder à l\'album
Minuteur de veille
@@ -521,14 +526,9 @@
Proposer à la Jam
- Aucun broker Jam configuré
- L\'hôte du broker est vide
Échec de la connexion
Connexion perdue
-
- Broker
- anonyme
Ajouté à la file d\'attente de la Jam
diff --git a/composeApp/src/commonMain/composeResources/values-hi/strings.xml b/composeApp/src/commonMain/composeResources/values-hi/strings.xml
index 29e2b1d2..5de37959 100644
--- a/composeApp/src/commonMain/composeResources/values-hi/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-hi/strings.xml
@@ -291,25 +291,11 @@
WebView
ऐप के अंदर वेब कंटेंट दिखाएं
ग्रुप जैम
- MQTT ब्रोकर
- ब्रोकर होस्ट
- %1$s:%2$d
- ब्रोकर पोर्ट
- TLS इस्तेमाल करें
- यूज़रनेम (वैकल्पिक)
- पासवर्ड (वैकल्पिक)
- क्लाइंट ID प्रीफ़िक्स
- इस Spotube की पहचान के लिए इस्तेमाल होता है
- ब्रोकर कनेक्शन एन्क्रिप्ट करें
- रूम शुरू करने से पहले मौजूदा ब्रोकर सेटिंग जांचें।
- कनेक्शन टेस्ट करें
- टेस्ट हो रहा है…
- %1$d ms में कनेक्ट हुआ
- विफल: %1$s
- प्लेसहोल्डर ब्रोकर — खुद होस्ट करने के लिए अपना ब्रोकर सेट करें
+ कनेक्शन टेस्ट करें
+ टेस्ट हो रहा है…
+ %1$d ms में कनेक्ट हुआ
+ विफल: %1$s
कनेक्शन
- क्रेडेंशियल
- पहचान
डायग्नोस्टिक्स
@@ -322,9 +308,6 @@
कनेक्ट हो रहा है…
रूम कोड
ABC123
- ब्रोकर: %1$s
- रूम होस्ट करने या उससे जुड़ने के लिए सेटिंग में ब्रोकर सेट करें।
- ब्रोकर सेटिंग
कनेक्ट है
फिर से कनेक्ट हो रहा है…
कोड कॉपी करें
@@ -344,6 +327,28 @@
जैम सेशन से नहीं जुड़ सके
अक्षरों और अंकों वाला छह अक्षर का रूम कोड डालें।
मेरे Spotube जैम से जुड़ें, कोड: %1$s
+ रूम सर्वर
+ सर्वर URL
+ https:// से शुरू होने वाला URL डालें
+ रूम शुरू करने से पहले रूम सर्वर जांच लें।
+ जैम Spotube के मुफ़्त रूम सर्वर का इस्तेमाल करता है। खुद होस्ट करने के लिए github.com/team-spotube/jam-server से सर्वर डिप्लॉय करें और उसका URL डालें।
+ आप
+ सर्वर: %1$s
+ रूम होस्ट करने या उसमें शामिल होने के लिए सेटिंग में रूम सर्वर सेट करें।
+ सर्वर सेटिंग
+ %1$s (आप)
+ आपका नाम
+ जैम में बाकी लोग आपको %1$s के रूप में देखते हैं
+ जैम में सभी को दिखता है। %1$s इस्तेमाल करने के लिए खाली छोड़ें। नया नाम अगली बार रूम बनाने या उसमें शामिल होने पर लागू होगा।
+ ज़्यादा से ज़्यादा %1$d अक्षर इस्तेमाल करें।
+ कोई जैम सर्वर सेट नहीं है
+ जैम सर्वर तक नहीं पहुंच पाए। अपना नेटवर्क या सेटिंग → ग्रुप जैम में सर्वर URL जांचें।
+ जैम सर्वर का वर्शन संगत नहीं है।
+ इस कोड वाला कोई रूम नहीं है। कोड जांचकर फिर से कोशिश करें।
+ यह रूम भर गया है।
+ होस्ट ने आपको इस रूम से बैन कर दिया है।
+ होस्ट ने आपको रूम से हटा दिया है।
+ यह रूम इस डिवाइस पर फिर से खोला गया, इसलिए यह कनेक्शन बंद कर दिया गया।
एल्बम पर जाएं
स्लीप टाइमर
@@ -516,14 +521,9 @@
जैम में सुझाएं
- कोई जैम ब्रोकर सेट नहीं है
- ब्रोकर होस्ट खाली है
कनेक्शन विफल
कनेक्शन टूट गया
-
- ब्रोकर
- anonymous
जैम कतार में जोड़ा गया
diff --git a/composeApp/src/commonMain/composeResources/values-id/strings.xml b/composeApp/src/commonMain/composeResources/values-id/strings.xml
index 3f46bc82..0cd52e2e 100644
--- a/composeApp/src/commonMain/composeResources/values-id/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-id/strings.xml
@@ -288,25 +288,11 @@
WebView
Menampilkan konten web di dalam aplikasi
Jam Grup
- Broker MQTT
- Host broker
- %1$s:%2$d
- Port broker
- Gunakan TLS
- Nama pengguna (opsional)
- Kata sandi (opsional)
- Prefiks ID klien
- Dipakai untuk mengidentifikasi Spotube ini
- Enkripsi koneksi broker
- Periksa pengaturan broker saat ini sebelum memulai ruang.
- Uji koneksi
- Menguji…
- Terhubung dalam %1$d ms
- Gagal: %1$s
- Broker sementara — konfigurasikan milikmu sendiri untuk self-host
+ Uji koneksi
+ Menguji…
+ Terhubung dalam %1$d ms
+ Gagal: %1$s
Koneksi
- Kredensial
- Identitas
Diagnostik
@@ -319,9 +305,6 @@
Menghubungkan…
Kode ruang
ABC123
- Broker: %1$s
- Atur broker di Pengaturan untuk membuat atau bergabung ke ruang.
- Pengaturan broker
Terhubung
Menghubungkan ulang…
Salin kode
@@ -341,6 +324,28 @@
Gagal bergabung ke sesi Jam
Masukkan kode ruang enam karakter berisi huruf dan angka.
Gabung ke Jam Spotube-ku dengan kode: %1$s
+ Server ruang
+ URL server
+ Masukkan URL yang diawali https://
+ Periksa server ruang sebelum memulai ruang.
+ Jam memakai server ruang gratis milik Spotube. Untuk meng-host sendiri, deploy server dari github.com/team-spotube/jam-server lalu masukkan URL-nya.
+ Kamu
+ Server: %1$s
+ Atur server ruang di Pengaturan untuk membuat atau bergabung ke ruang.
+ Pengaturan server
+ %1$s (kamu)
+ Namamu
+ Orang lain di jam melihatmu sebagai %1$s
+ Ditampilkan ke semua orang di jam. Biarkan kosong untuk memakai %1$s. Nama baru berlaku saat kamu berikutnya membuat atau bergabung ke ruang.
+ Gunakan paling banyak %1$d karakter.
+ Belum ada server jam yang diatur
+ Tidak bisa menjangkau server jam. Periksa jaringan atau URL server di Pengaturan → Jam Grup.
+ Server jam menjalankan versi yang tidak kompatibel.
+ Tidak ada ruang dengan kode itu. Periksa lalu coba lagi.
+ Ruang ini sudah penuh.
+ Host memblokirmu dari ruang ini.
+ Host mengeluarkanmu dari ruang.
+ Ruang ini dibuka lagi di perangkat ini, jadi koneksi ini ditutup.
Buka album
Timer tidur
@@ -513,14 +518,9 @@
Sarankan ke Jam
- Belum ada broker Jam yang dikonfigurasi
- Host broker kosong
Koneksi gagal
Koneksi terputus
-
- Broker
- anonim
Ditambahkan ke antrean Jam
diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml
index 2d956a2c..5ddcc190 100644
--- a/composeApp/src/commonMain/composeResources/values-it/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml
@@ -296,25 +296,11 @@
WebView
Mostrare contenuti web all\'interno dell\'app
Jam di gruppo
- Broker MQTT
- Host del broker
- %1$s:%2$d
- Porta del broker
- Usa TLS
- Nome utente (facoltativo)
- Password (facoltativa)
- Prefisso ID client
- Usato per identificare questa istanza di Spotube
- Crittografa la connessione al broker
- Verifica le impostazioni attuali del broker prima di avviare una stanza.
- Verifica connessione
- Verifica in corso…
- Connesso in %1$d ms
- Non riuscito: %1$s
- Broker di esempio: configurane uno tuo per l\'hosting autonomo
+ Verifica connessione
+ Verifica in corso…
+ Connesso in %1$d ms
+ Non riuscito: %1$s
Connessione
- Credenziali
- Identità
Diagnostica
@@ -327,9 +313,6 @@
Connessione…
Codice stanza
ABC123
- Broker: %1$s
- Configura un broker nelle Impostazioni per ospitare una stanza o parteciparvi.
- Impostazioni broker
Connesso
Riconnessione…
Copia codice
@@ -349,6 +332,28 @@
Impossibile partecipare alla sessione Jam
Inserisci un codice stanza di sei caratteri composto da lettere e cifre.
Unisciti alla mia Jam su Spotube con il codice: %1$s
+ Server delle stanze
+ URL del server
+ Inserisci un URL che inizi con https://
+ Controlla il server delle stanze prima di avviare una stanza.
+ Jam usa il server delle stanze gratuito di Spotube. Per ospitarlo in proprio, distribuisci il server da github.com/team-spotube/jam-server e inseriscine l’URL.
+ Tu
+ Server: %1$s
+ Imposta un server delle stanze nelle Impostazioni per ospitare una stanza o entrarci.
+ Impostazioni del server
+ %1$s (tu)
+ Il tuo nome
+ Gli altri nella jam ti vedono come %1$s
+ Visibile a tutti nella jam. Lascia vuoto per usare %1$s. Un nuovo nome vale dalla prossima volta che avvii una stanza o ci entri.
+ Usa al massimo %1$d caratteri.
+ Nessun server jam configurato
+ Impossibile raggiungere il server jam. Controlla la rete o l’URL del server in Impostazioni → Jam di gruppo.
+ Il server jam usa una versione non compatibile.
+ Nessuna stanza con questo codice. Controllalo e riprova.
+ Questa stanza è piena.
+ L’host ti ha bannato da questa stanza.
+ L’host ti ha rimosso dalla stanza.
+ Questa stanza è stata riaperta su questo dispositivo, quindi questa connessione è stata chiusa.
Vai all\'album
Timer di spegnimento
@@ -521,14 +526,9 @@
Suggerisci alla Jam
- Nessun broker Jam configurato
- L\'host del broker è vuoto
Connessione non riuscita
Connessione persa
-
- Broker
- anonimo
Aggiunto alla coda della Jam
diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings.xml b/composeApp/src/commonMain/composeResources/values-ja/strings.xml
index 27ea528a..1464d695 100644
--- a/composeApp/src/commonMain/composeResources/values-ja/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ja/strings.xml
@@ -288,25 +288,11 @@
WebView
アプリ内でのWebコンテンツの表示
グループジャム
- MQTTブローカー
- ブローカーのホスト
- %1$s:%2$d
- ブローカーのポート
- TLSを使用
- ユーザー名 (任意)
- パスワード (任意)
- クライアントIDのプレフィックス
- このSpotubeを識別するために使用されます
- ブローカーとの接続を暗号化します
- ルームを開始する前に、現在のブローカー設定を確認します。
- 接続をテスト
- テスト中…
- %1$d msで接続しました
- 失敗: %1$s
- 仮のブローカーです。セルフホストするには独自のブローカーを設定してください
+ 接続をテスト
+ テスト中…
+ %1$d msで接続しました
+ 失敗: %1$s
接続
- 認証情報
- 識別情報
診断
@@ -319,9 +305,6 @@
接続中…
ルームコード
ABC123
- ブローカー: %1$s
- ルームを主催または参加するには、設定でブローカーを設定してください。
- ブローカー設定
接続済み
再接続中…
コードをコピー
@@ -341,6 +324,28 @@
ジャムセッションに参加できませんでした
英数字6文字のルームコードを入力してください。
コード %1$s で私のSpotubeジャムに参加しよう
+ ルームサーバー
+ サーバーURL
+ https:// で始まるURLを入力してください。
+ ルームを始める前にルームサーバーを確認します。
+ ジャムはSpotubeの無料ルームサーバーを使います。自分でホストするには、github.com/team-spotube/jam-server のサーバーをデプロイし、そのURLを入力してください。
+ あなた
+ サーバー: %1$s
+ ルームをホストまたは参加するには、設定でルームサーバーを指定してください。
+ サーバー設定
+ %1$s(あなた)
+ あなたの名前
+ ジャムの他の参加者には %1$s と表示されます
+ ジャムの全員に表示されます。空欄にすると %1$s を使います。新しい名前は次にルームを作成または参加したときから使われます。
+ %1$d 文字以内で入力してください。
+ ジャムサーバーが設定されていません
+ ジャムサーバーに接続できません。ネットワーク、または 設定 → グループジャム のサーバーURLを確認してください。
+ ジャムサーバーのバージョンに互換性がありません。
+ そのコードのルームはありません。コードを確認してもう一度お試しください。
+ このルームは満員です。
+ ホストによってこのルームからBANされました。
+ ホストによってルームから削除されました。
+ このデバイスでこのルームが再度開かれたため、この接続は閉じられました。
アルバムへ移動
スリープタイマー
@@ -513,14 +518,9 @@
ジャムに提案
- ジャム用ブローカーが設定されていません
- ブローカーのホストが空です
接続に失敗しました
接続が切断されました
-
- ブローカー
- anonymous
ジャムのキューに追加しました
diff --git a/composeApp/src/commonMain/composeResources/values-ko/strings.xml b/composeApp/src/commonMain/composeResources/values-ko/strings.xml
index 67fb729e..39b8854c 100644
--- a/composeApp/src/commonMain/composeResources/values-ko/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ko/strings.xml
@@ -288,25 +288,11 @@
WebView
앱 안에 웹 콘텐츠 표시
그룹 잼
- MQTT 브로커
- 브로커 호스트
- %1$s:%2$d
- 브로커 포트
- TLS 사용
- 사용자 이름(선택 사항)
- 비밀번호(선택 사항)
- 클라이언트 ID 접두사
- 이 Spotube 인스턴스를 식별하는 데 사용돼요
- 브로커 연결 암호화
- 방을 시작하기 전에 현재 브로커 설정을 확인하세요.
- 연결 테스트
- 테스트 중…
- %1$d ms 만에 연결됨
- 실패: %1$s
- 임시 브로커입니다 — 직접 호스팅하려면 브로커를 설정하세요
+ 연결 테스트
+ 테스트 중…
+ %1$d ms 만에 연결됨
+ 실패: %1$s
연결
- 인증 정보
- 식별 정보
진단
@@ -319,9 +305,6 @@
연결 중…
방 코드
ABC123
- 브로커: %1$s
- 방을 만들거나 참여하려면 설정에서 브로커를 설정하세요.
- 브로커 설정
연결됨
다시 연결 중…
코드 복사
@@ -341,6 +324,28 @@
잼 세션에 참여하지 못했어요
영문자와 숫자로 된 6자리 방 코드를 입력하세요.
내 Spotube 잼에 참여하세요. 코드: %1$s
+ 방 서버
+ 서버 URL
+ https://로 시작하는 URL을 입력하세요.
+ 방을 시작하기 전에 방 서버를 확인하세요.
+ 잼은 Spotube의 무료 방 서버를 사용합니다. 직접 호스팅하려면 github.com/team-spotube/jam-server의 서버를 배포하고 URL을 입력하세요.
+ 나
+ 서버: %1$s
+ 방을 만들거나 참여하려면 설정에서 방 서버를 지정하세요.
+ 서버 설정
+ %1$s (나)
+ 내 이름
+ 잼의 다른 사람들에게 %1$s(으)로 표시됩니다
+ 잼의 모든 사람에게 표시됩니다. 비워 두면 %1$s을(를) 사용합니다. 새 이름은 다음에 방을 만들거나 참여할 때부터 적용됩니다.
+ 최대 %1$d자까지 입력하세요.
+ 설정된 잼 서버가 없습니다
+ 잼 서버에 연결할 수 없습니다. 네트워크 또는 설정 → 그룹 잼의 서버 URL을 확인하세요.
+ 잼 서버의 버전이 호환되지 않습니다.
+ 해당 코드의 방이 없습니다. 코드를 확인하고 다시 시도하세요.
+ 이 방은 가득 찼습니다.
+ 호스트가 이 방에서 나를 차단했습니다.
+ 호스트가 나를 방에서 내보냈습니다.
+ 이 기기에서 이 방이 다시 열려 이 연결이 종료되었습니다.
앨범으로 이동
취침 타이머
@@ -513,14 +518,9 @@
잼에 추천하기
- 구성된 잼 브로커가 없어요
- 브로커 호스트가 비어 있어요
연결 실패
연결 끊김
-
- 브로커
- 익명
잼 대기열에 추가했어요
diff --git a/composeApp/src/commonMain/composeResources/values-nl/strings.xml b/composeApp/src/commonMain/composeResources/values-nl/strings.xml
index a5e4197e..e3520c74 100644
--- a/composeApp/src/commonMain/composeResources/values-nl/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-nl/strings.xml
@@ -292,25 +292,11 @@
WebView
Webcontent weergeven in de app
Group Jam
- MQTT-broker
- Broker-host
- %1$s:%2$d
- Broker-poort
- TLS gebruiken
- Gebruikersnaam (optioneel)
- Wachtwoord (optioneel)
- Voorvoegsel client-ID
- Wordt gebruikt om deze Spotube-installatie te herkennen
- De verbinding met de broker versleutelen
- Controleer de huidige broker-instellingen voordat je een Jam start.
- Verbinding testen
- Testen…
- Verbonden in %1$d ms
- Mislukt: %1$s
- Tijdelijke broker — stel je eigen broker in om zelf te hosten
+ Verbinding testen
+ Testen…
+ Verbonden in %1$d ms
+ Mislukt: %1$s
Verbinding
- Inloggegevens
- Identiteit
Diagnose
@@ -323,9 +309,6 @@
Verbinden…
Sessiecode
ABC123
- Broker: %1$s
- Stel een broker in bij Instellingen om een sessie te hosten of eraan deel te nemen.
- Broker-instellingen
Verbonden
Opnieuw verbinden…
Code kopiëren
@@ -345,6 +328,28 @@
Deelnemen aan Jam-sessie mislukt
Voer een sessiecode van zes tekens in met letters en cijfers.
Doe mee met mijn Spotube Jam met code: %1$s
+ Sessieserver
+ Server-URL
+ Voer een URL in die begint met https://
+ Controleer de sessieserver voordat je een sessie start.
+ Jam gebruikt de gratis sessieserver van Spotube. Wil je hem zelf hosten, deploy dan de server van github.com/team-spotube/jam-server en voer de URL in.
+ Jij
+ Server: %1$s
+ Stel in Instellingen een sessieserver in om een sessie te hosten of eraan deel te nemen.
+ Serverinstellingen
+ %1$s (jij)
+ Je naam
+ Anderen in de jam zien je als %1$s
+ Zichtbaar voor iedereen in de jam. Laat leeg om %1$s te gebruiken. Een nieuwe naam geldt vanaf de volgende keer dat je een sessie start of eraan deelneemt.
+ Gebruik maximaal %1$d tekens.
+ Geen jamserver ingesteld
+ Kan de jamserver niet bereiken. Controleer je netwerk of de server-URL in Instellingen → Group Jam.
+ De jamserver draait een incompatibele versie.
+ Geen sessie met die code. Controleer de code en probeer het opnieuw.
+ Deze sessie is vol.
+ De host heeft je uit deze sessie verbannen.
+ De host heeft je uit de sessie verwijderd.
+ Deze sessie is opnieuw geopend op dit apparaat, dus deze verbinding is gesloten.
Naar album
Slaaptimer
@@ -517,14 +522,9 @@
Voorstellen aan Jam
- Geen Jam-broker ingesteld
- Broker-host is leeg
Verbinding mislukt
Verbinding verbroken
-
- Broker
- anoniem
Toegevoegd aan de Jam-wachtrij
diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml
index 5af41a10..92c9b443 100644
--- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml
@@ -300,25 +300,11 @@
WebView
Wyświetlanie treści internetowych w aplikacji
Grupowy Jam
- Broker MQTT
- Host brokera
- %1$s:%2$d
- Port brokera
- Użyj TLS
- Nazwa użytkownika (opcjonalnie)
- Hasło (opcjonalnie)
- Prefiks ID klienta
- Służy do identyfikacji tej instancji Spotube
- Szyfruj połączenie z brokerem
- Sprawdź bieżące ustawienia brokera przed utworzeniem pokoju.
- Testuj połączenie
- Testowanie…
- Połączono w %1$d ms
- Niepowodzenie: %1$s
- Broker zastępczy – skonfiguruj własny, aby hostować samodzielnie
+ Testuj połączenie
+ Testowanie…
+ Połączono w %1$d ms
+ Niepowodzenie: %1$s
Połączenie
- Dane logowania
- Tożsamość
Diagnostyka
@@ -331,9 +317,6 @@
Łączenie…
Kod pokoju
ABC123
- Broker: %1$s
- Skonfiguruj brokera w Ustawieniach, aby utworzyć pokój lub do niego dołączyć.
- Ustawienia brokera
Połączono
Ponowne łączenie…
Kopiuj kod
@@ -353,6 +336,28 @@
Nie udało się dołączyć do sesji Jam
Wpisz sześcioznakowy kod pokoju złożony z liter i cyfr.
Dołącz do mojego Jamu w Spotube, kod: %1$s
+ Serwer pokojów
+ Adres URL serwera
+ Wpisz adres URL zaczynający się od https://
+ Sprawdź serwer pokojów przed utworzeniem pokoju.
+ Jam korzysta z darmowego serwera pokojów Spotube. Aby hostować własny, wdróż serwer z github.com/team-spotube/jam-server i wpisz jego adres URL.
+ Ty
+ Serwer: %1$s
+ Ustaw serwer pokojów w Ustawieniach, aby utworzyć pokój lub do niego dołączyć.
+ Ustawienia serwera
+ %1$s (ty)
+ Twoja nazwa
+ Inni w jamie widzą cię jako %1$s
+ Widoczna dla wszystkich w jamie. Zostaw puste, aby użyć %1$s. Nowa nazwa zacznie obowiązywać przy następnym utworzeniu pokoju lub dołączeniu do niego.
+ Użyj maksymalnie %1$d znaków.
+ Nie skonfigurowano serwera jamu
+ Nie można połączyć się z serwerem jamu. Sprawdź sieć lub adres URL serwera w Ustawienia → Grupowy Jam.
+ Serwer jamu używa niezgodnej wersji.
+ Nie ma pokoju o tym kodzie. Sprawdź go i spróbuj ponownie.
+ Ten pokój jest pełny.
+ Gospodarz zablokował cię w tym pokoju.
+ Gospodarz usunął cię z pokoju.
+ Ten pokój został ponownie otwarty na tym urządzeniu, więc to połączenie zostało zamknięte.
Przejdź do albumu
Wyłącznik czasowy
@@ -525,14 +530,9 @@
Zaproponuj w Jamie
- Nie skonfigurowano brokera Jam
- Host brokera jest pusty
Połączenie nieudane
Utracono połączenie
-
- Broker
- anonim
Dodano do kolejki Jamu
diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings.xml b/composeApp/src/commonMain/composeResources/values-pt/strings.xml
index ce987e2b..0f971be6 100644
--- a/composeApp/src/commonMain/composeResources/values-pt/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-pt/strings.xml
@@ -296,25 +296,11 @@
WebView
Exibir conteúdo da web dentro do app
Jam em grupo
- Broker MQTT
- Host do broker
- %1$s:%2$d
- Porta do broker
- Usar TLS
- Nome de usuário (opcional)
- Senha (opcional)
- Prefixo do ID do cliente
- Usado para identificar esta instância do Spotube
- Criptografa a conexão com o broker
- Verifique as configurações atuais do broker antes de criar uma sala.
- Testar conexão
- Testando…
- Conectado em %1$d ms
- Falhou: %1$s
- Broker provisório — configure o seu próprio para hospedar por conta própria
+ Testar conexão
+ Testando…
+ Conectado em %1$d ms
+ Falhou: %1$s
Conexão
- Credenciais
- Identidade
Diagnóstico
@@ -327,9 +313,6 @@
Conectando…
Código da sala
ABC123
- Broker: %1$s
- Configure um broker nas Configurações para criar ou entrar em uma sala.
- Configurações do broker
Conectado
Reconectando…
Copiar código
@@ -349,6 +332,28 @@
Falha ao entrar na Jam
Digite um código de sala com seis caracteres, usando letras e números.
Entre na minha Jam do Spotube com o código: %1$s
+ Servidor de salas
+ URL do servidor
+ Digite uma URL que comece com https://
+ Verifique o servidor de salas antes de criar uma sala.
+ O Jam usa o servidor de salas gratuito do Spotube. Para hospedar o seu, implante o servidor de github.com/team-spotube/jam-server e informe a URL dele.
+ Você
+ Servidor: %1$s
+ Defina um servidor de salas nas Configurações para criar ou entrar em uma sala.
+ Configurações do servidor
+ %1$s (você)
+ Seu nome
+ Os outros no jam veem você como %1$s
+ Visível para todos no jam. Deixe em branco para usar %1$s. Um novo nome vale a partir da próxima vez que você criar ou entrar em uma sala.
+ Use no máximo %1$d caracteres.
+ Nenhum servidor de jam configurado
+ Não foi possível acessar o servidor de jam. Verifique sua rede ou a URL do servidor em Configurações → Jam em grupo.
+ O servidor de jam usa uma versão incompatível.
+ Nenhuma sala com esse código. Confira e tente de novo.
+ Esta sala está cheia.
+ O anfitrião baniu você desta sala.
+ O anfitrião removeu você da sala.
+ Esta sala foi aberta de novo neste dispositivo, então esta conexão foi encerrada.
Ir para o álbum
Timer
@@ -521,14 +526,9 @@
Sugerir na Jam
- Nenhum broker de Jam configurado
- O host do broker está vazio
Falha na conexão
Conexão perdida
-
- Broker
- anônimo
Adicionada à fila da Jam
diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings.xml b/composeApp/src/commonMain/composeResources/values-ru/strings.xml
index 8128a8ce..b413416b 100644
--- a/composeApp/src/commonMain/composeResources/values-ru/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ru/strings.xml
@@ -300,25 +300,11 @@
WebView
Показ веб-контента внутри приложения
Групповой джем
- MQTT-брокер
- Хост брокера
- %1$s:%2$d
- Порт брокера
- Использовать TLS
- Имя пользователя (необязательно)
- Пароль (необязательно)
- Префикс ID клиента
- Используется для идентификации этого экземпляра Spotube
- Шифровать подключение к брокеру
- Проверьте текущие настройки брокера перед созданием комнаты.
- Проверить подключение
- Проверка…
- Подключено за %1$d мс
- Ошибка: %1$s
- Временный брокер — настройте свой, чтобы размещать джемы самостоятельно
+ Проверить подключение
+ Проверка…
+ Подключено за %1$d мс
+ Ошибка: %1$s
Подключение
- Учётные данные
- Идентификация
Диагностика
@@ -331,9 +317,6 @@
Подключение…
Код комнаты
ABC123
- Брокер: %1$s
- Настройте брокер в настройках, чтобы создать комнату или присоединиться к ней.
- Настройки брокера
Подключено
Переподключение…
Копировать код
@@ -353,6 +336,28 @@
Не удалось присоединиться к джему
Введите код комнаты из шести букв и цифр.
Присоединяйся к моему джему в Spotube по коду: %1$s
+ Сервер комнат
+ URL сервера
+ Введите URL, начинающийся с https://
+ Проверьте сервер комнат перед созданием комнаты.
+ Джем использует бесплатный сервер комнат Spotube. Чтобы развернуть свой, установите сервер из github.com/team-spotube/jam-server и укажите его URL.
+ Вы
+ Сервер: %1$s
+ Укажите сервер комнат в настройках, чтобы создать комнату или присоединиться к ней.
+ Настройки сервера
+ %1$s (вы)
+ Ваше имя
+ Другие участники джема видят вас как %1$s
+ Видно всем в джеме. Оставьте пустым, чтобы использовать %1$s. Новое имя применится, когда вы в следующий раз создадите комнату или присоединитесь к ней.
+ Не больше %1$d символов.
+ Сервер джема не настроен
+ Не удалось подключиться к серверу джема. Проверьте сеть или URL сервера в разделе Настройки → Групповой джем.
+ Сервер джема использует несовместимую версию.
+ Комнаты с таким кодом нет. Проверьте код и попробуйте снова.
+ Эта комната заполнена.
+ Организатор заблокировал вас в этой комнате.
+ Организатор удалил вас из комнаты.
+ Эта комната была снова открыта на этом устройстве, поэтому это подключение закрыто.
Перейти к альбому
Таймер сна
@@ -525,14 +530,9 @@
Предложить в джем
- Брокер для джема не настроен
- Не указан хост брокера
Не удалось подключиться
Соединение потеряно
-
- Брокер
- anonymous
Добавлено в очередь джема
diff --git a/composeApp/src/commonMain/composeResources/values-th/strings.xml b/composeApp/src/commonMain/composeResources/values-th/strings.xml
index ab470a92..6498a53b 100644
--- a/composeApp/src/commonMain/composeResources/values-th/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-th/strings.xml
@@ -288,25 +288,11 @@
WebView
แสดงเนื้อหาเว็บภายในแอป
แจมกลุ่ม
- MQTT Broker
- โฮสต์ของ Broker
- %1$s:%2$d
- พอร์ตของ Broker
- ใช้ TLS
- ชื่อผู้ใช้ (ไม่บังคับ)
- รหัสผ่าน (ไม่บังคับ)
- คำนำหน้า Client ID
- ใช้เพื่อระบุ Spotube เครื่องนี้
- เข้ารหัสการเชื่อมต่อกับ Broker
- ตรวจสอบการตั้งค่า Broker ปัจจุบันก่อนเริ่มห้อง
- ทดสอบการเชื่อมต่อ
- กำลังทดสอบ…
- เชื่อมต่อแล้วใน %1$d ms
- ล้มเหลว: %1$s
- Broker ตัวอย่าง — กำหนดค่าของคุณเองเพื่อโฮสต์เอง
+ ทดสอบการเชื่อมต่อ
+ กำลังทดสอบ…
+ เชื่อมต่อแล้วใน %1$d ms
+ ล้มเหลว: %1$s
การเชื่อมต่อ
- ข้อมูลเข้าสู่ระบบ
- ตัวตน
การวินิจฉัย
@@ -319,9 +305,6 @@
กำลังเชื่อมต่อ…
รหัสห้อง
ABC123
- Broker: %1$s
- ตั้งค่า Broker ในการตั้งค่าเพื่อสร้างหรือเข้าร่วมห้อง
- การตั้งค่า Broker
เชื่อมต่อแล้ว
กำลังเชื่อมต่อใหม่…
คัดลอกรหัส
@@ -341,6 +324,28 @@
เข้าร่วมเซสชันแจมไม่สำเร็จ
ใส่รหัสห้อง 6 ตัวอักษร ที่ประกอบด้วยตัวอักษรและตัวเลข
มาร่วมแจมกับฉันบน Spotube ด้วยรหัส: %1$s
+ เซิร์ฟเวอร์ห้อง
+ URL ของเซิร์ฟเวอร์
+ ใส่ URL ที่ขึ้นต้นด้วย https://
+ ตรวจสอบเซิร์ฟเวอร์ห้องก่อนเริ่มห้อง
+ แจมใช้เซิร์ฟเวอร์ห้องฟรีของ Spotube หากต้องการโฮสต์เอง ให้ติดตั้งเซิร์ฟเวอร์จาก github.com/team-spotube/jam-server แล้วใส่ URL ของเซิร์ฟเวอร์นั้น
+ คุณ
+ เซิร์ฟเวอร์: %1$s
+ ตั้งค่าเซิร์ฟเวอร์ห้องในการตั้งค่าเพื่อโฮสต์หรือเข้าร่วมห้อง
+ การตั้งค่าเซิร์ฟเวอร์
+ %1$s (คุณ)
+ ชื่อของคุณ
+ คนอื่นในแจมจะเห็นคุณเป็น %1$s
+ ทุกคนในแจมจะเห็นชื่อนี้ เว้นว่างไว้เพื่อใช้ %1$s ชื่อใหม่จะมีผลเมื่อคุณสร้างหรือเข้าร่วมห้องครั้งถัดไป
+ ใช้ได้ไม่เกิน %1$d ตัวอักษร
+ ยังไม่ได้ตั้งค่าเซิร์ฟเวอร์แจม
+ เชื่อมต่อเซิร์ฟเวอร์แจมไม่ได้ ตรวจสอบเครือข่ายหรือ URL ของเซิร์ฟเวอร์ใน การตั้งค่า → แจมกลุ่ม
+ เซิร์ฟเวอร์แจมใช้เวอร์ชันที่เข้ากันไม่ได้
+ ไม่มีห้องที่ใช้รหัสนี้ ตรวจสอบแล้วลองอีกครั้ง
+ ห้องนี้เต็มแล้ว
+ โฮสต์แบนคุณออกจากห้องนี้
+ โฮสต์นำคุณออกจากห้อง
+ ห้องนี้ถูกเปิดอีกครั้งบนอุปกรณ์นี้ จึงปิดการเชื่อมต่อนี้
ไปที่อัลบั้ม
ตั้งเวลาปิด
@@ -513,14 +518,9 @@
เสนอให้แจม
- ยังไม่ได้กำหนดค่า Broker สำหรับแจม
- โฮสต์ของ Broker ว่างอยู่
การเชื่อมต่อล้มเหลว
การเชื่อมต่อขาดหาย
-
- Broker
- anonymous
เพิ่มลงในคิวแจมแล้ว
diff --git a/composeApp/src/commonMain/composeResources/values-tr/strings.xml b/composeApp/src/commonMain/composeResources/values-tr/strings.xml
index 54d5ed93..fe357c8c 100644
--- a/composeApp/src/commonMain/composeResources/values-tr/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-tr/strings.xml
@@ -292,25 +292,11 @@
WebView
Uygulama içinde web içeriği görüntüleme
Grup Jam
- MQTT Aracısı
- Aracı ana bilgisayarı
- %1$s:%2$d
- Aracı portu
- TLS kullan
- Kullanıcı adı (isteğe bağlı)
- Şifre (isteğe bağlı)
- İstemci kimliği ön eki
- Bu Spotube örneğini tanımlamak için kullanılır
- Aracı bağlantısını şifrele
- Oda başlatmadan önce geçerli aracı ayarlarını kontrol et.
- Bağlantıyı test et
- Test ediliyor…
- %1$d ms\'de bağlandı
- Başarısız: %1$s
- Geçici aracı — kendin barındırmak için kendi aracını yapılandır
+ Bağlantıyı test et
+ Test ediliyor…
+ %1$d ms\'de bağlandı
+ Başarısız: %1$s
Bağlantı
- Kimlik bilgileri
- Kimlik
Tanılama
@@ -323,9 +309,6 @@
Bağlanıyor…
Oda kodu
ABC123
- Aracı: %1$s
- Oda açmak veya bir odaya katılmak için Ayarlar\'da bir aracı ayarla.
- Aracı ayarları
Bağlandı
Yeniden bağlanıyor…
Kodu kopyala
@@ -345,6 +328,28 @@
Jam oturumuna katılınamadı
Harf ve rakamlardan oluşan altı karakterli bir oda kodu gir.
Spotube Jam\'ime şu kodla katıl: %1$s
+ Oda sunucusu
+ Sunucu URL’si
+ https:// ile başlayan bir URL gir.
+ Oda başlatmadan önce oda sunucusunu kontrol et.
+ Jam, Spotube’un ücretsiz oda sunucusunu kullanır. Kendin barındırmak için github.com/team-spotube/jam-server adresindeki sunucuyu kur ve URL’sini gir.
+ Sen
+ Sunucu: %1$s
+ Oda açmak veya bir odaya katılmak için Ayarlar’da bir oda sunucusu belirle.
+ Sunucu ayarları
+ %1$s (sen)
+ Adın
+ Jam’deki diğer kişiler seni %1$s olarak görür
+ Jam’deki herkese gösterilir. %1$s adını kullanmak için boş bırak. Yeni ad, bir sonraki oda açışında veya katılışında geçerli olur.
+ En fazla %1$d karakter kullan.
+ Jam sunucusu ayarlanmamış
+ Jam sunucusuna ulaşılamadı. Ağını veya Ayarlar → Grup Jam bölümündeki sunucu URL’sini kontrol et.
+ Jam sunucusu uyumsuz bir sürüm çalıştırıyor.
+ Bu koda sahip bir oda yok. Kodu kontrol edip tekrar dene.
+ Bu oda dolu.
+ Oda sahibi seni bu odadan engelledi.
+ Oda sahibi seni odadan çıkardı.
+ Bu oda bu cihazda yeniden açıldı, bu yüzden bu bağlantı kapatıldı.
Albüme git
Uyku zamanlayıcısı
@@ -517,14 +522,9 @@
Jam\'e öner
- Jam aracısı yapılandırılmadı
- Aracı ana bilgisayarı boş
Bağlantı başarısız
Bağlantı kesildi
-
- Aracı
- anonymous
Jam sırasına eklendi
diff --git a/composeApp/src/commonMain/composeResources/values-uk/strings.xml b/composeApp/src/commonMain/composeResources/values-uk/strings.xml
index 0fd2d19b..966a429c 100644
--- a/composeApp/src/commonMain/composeResources/values-uk/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-uk/strings.xml
@@ -300,25 +300,11 @@
WebView
Показ вебвмісту всередині застосунку
Груповий джем
- MQTT-брокер
- Хост брокера
- %1$s:%2$d
- Порт брокера
- Використовувати TLS
- Ім’я користувача (необов’язково)
- Пароль (необов’язково)
- Префікс ID клієнта
- Використовується для ідентифікації цього екземпляра Spotube
- Шифрувати підключення до брокера
- Перевірте поточні налаштування брокера перед створенням кімнати.
- Перевірити підключення
- Перевірка…
- Підключено за %1$d мс
- Помилка: %1$s
- Тимчасовий брокер — налаштуйте власний, щоб розміщувати джеми самостійно
+ Перевірити підключення
+ Перевірка…
+ Підключено за %1$d мс
+ Помилка: %1$s
Підключення
- Облікові дані
- Ідентифікація
Діагностика
@@ -331,9 +317,6 @@
Підключення…
Код кімнати
ABC123
- Брокер: %1$s
- Налаштуйте брокер у налаштуваннях, щоб створити кімнату або приєднатися до неї.
- Налаштування брокера
Підключено
Повторне підключення…
Копіювати код
@@ -353,6 +336,28 @@
Не вдалося приєднатися до джему
Введіть код кімнати з шести літер і цифр.
Приєднуйся до мого джему в Spotube за кодом: %1$s
+ Сервер кімнат
+ URL сервера
+ Введіть URL, що починається з https://
+ Перевірте сервер кімнат перед створенням кімнати.
+ Джем використовує безкоштовний сервер кімнат Spotube. Щоб розгорнути власний, встановіть сервер із github.com/team-spotube/jam-server і вкажіть його URL.
+ Ви
+ Сервер: %1$s
+ Укажіть сервер кімнат у налаштуваннях, щоб створити кімнату або приєднатися до неї.
+ Налаштування сервера
+ %1$s (ви)
+ Ваше ім’я
+ Інші учасники джему бачать вас як %1$s
+ Видно всім у джемі. Залиште порожнім, щоб використовувати %1$s. Нове ім’я застосується, коли ви наступного разу створите кімнату або приєднаєтеся до неї.
+ Не більше %1$d символів.
+ Сервер джему не налаштовано
+ Не вдалося підключитися до сервера джему. Перевірте мережу або URL сервера в розділі Налаштування → Груповий джем.
+ Сервер джему використовує несумісну версію.
+ Кімнати з таким кодом немає. Перевірте код і спробуйте ще раз.
+ Ця кімната заповнена.
+ Організатор заблокував вас у цій кімнаті.
+ Організатор видалив вас із кімнати.
+ Цю кімнату знову відкрито на цьому пристрої, тому це підключення закрито.
Перейти до альбому
Таймер сну
@@ -525,14 +530,9 @@
Запропонувати в джем
- Брокер для джему не налаштовано
- Не вказано хост брокера
Не вдалося підключитися
З’єднання втрачено
-
- Брокер
- anonymous
Додано до черги джему
diff --git a/composeApp/src/commonMain/composeResources/values-vi/strings.xml b/composeApp/src/commonMain/composeResources/values-vi/strings.xml
index 39ae5edc..7866e671 100644
--- a/composeApp/src/commonMain/composeResources/values-vi/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-vi/strings.xml
@@ -288,25 +288,11 @@
WebView
Hiển thị nội dung web trong ứng dụng
Jam nhóm
- MQTT Broker
- Máy chủ broker
- %1$s:%2$d
- Cổng broker
- Dùng TLS
- Tên người dùng (không bắt buộc)
- Mật khẩu (không bắt buộc)
- Tiền tố Client ID
- Dùng để nhận diện Spotube này
- Mã hóa kết nối tới broker
- Kiểm tra cài đặt broker hiện tại trước khi tạo phòng.
- Kiểm tra kết nối
- Đang kiểm tra…
- Đã kết nối trong %1$d ms
- Thất bại: %1$s
- Broker tạm thời — hãy cấu hình broker riêng để tự lưu trữ
+ Kiểm tra kết nối
+ Đang kiểm tra…
+ Đã kết nối trong %1$d ms
+ Thất bại: %1$s
Kết nối
- Thông tin đăng nhập
- Danh tính
Chẩn đoán
@@ -319,9 +305,6 @@
Đang kết nối…
Mã phòng
ABC123
- Broker: %1$s
- Hãy thiết lập broker trong Cài đặt để tạo hoặc tham gia phòng.
- Cài đặt broker
Đã kết nối
Đang kết nối lại…
Sao chép mã
@@ -341,6 +324,28 @@
Không thể tham gia phiên Jam
Nhập mã phòng gồm sáu ký tự chữ và số.
Tham gia Jam Spotube của mình với mã: %1$s
+ Máy chủ phòng
+ URL máy chủ
+ Nhập URL bắt đầu bằng https://
+ Kiểm tra máy chủ phòng trước khi tạo phòng.
+ Jam dùng máy chủ phòng miễn phí của Spotube. Để tự lưu trữ, hãy triển khai máy chủ từ github.com/team-spotube/jam-server rồi nhập URL của nó.
+ Bạn
+ Máy chủ: %1$s
+ Đặt máy chủ phòng trong Cài đặt để tạo hoặc tham gia phòng.
+ Cài đặt máy chủ
+ %1$s (bạn)
+ Tên của bạn
+ Mọi người trong jam thấy bạn là %1$s
+ Hiển thị cho mọi người trong jam. Để trống để dùng %1$s. Tên mới sẽ áp dụng từ lần tới bạn tạo hoặc tham gia phòng.
+ Dùng tối đa %1$d ký tự.
+ Chưa cấu hình máy chủ jam
+ Không kết nối được máy chủ jam. Hãy kiểm tra mạng hoặc URL máy chủ trong Cài đặt → Jam nhóm.
+ Máy chủ jam đang chạy phiên bản không tương thích.
+ Không có phòng nào với mã này. Hãy kiểm tra và thử lại.
+ Phòng này đã đầy.
+ Chủ phòng đã cấm bạn khỏi phòng này.
+ Chủ phòng đã xóa bạn khỏi phòng.
+ Phòng này đã được mở lại trên thiết bị này nên kết nối này đã bị đóng.
Đi tới album
Hẹn giờ tắt
@@ -513,14 +518,9 @@
Đề xuất cho Jam
- Chưa cấu hình broker cho Jam
- Máy chủ broker đang trống
Kết nối thất bại
Mất kết nối
-
- Broker
- ẩn danh
Đã thêm vào danh sách chờ của Jam
diff --git a/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml b/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml
index 2f59a04d..0bbee39f 100644
--- a/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml
@@ -288,25 +288,11 @@
WebView
在應用程式內顯示網頁內容
群組 Jam
- MQTT 代理伺服器
- 代理伺服器主機
- %1$s:%2$d
- 代理伺服器連接埠
- 使用 TLS
- 使用者名稱(選填)
- 密碼(選填)
- 用戶端 ID 前綴
- 用於識別這個 Spotube
- 加密與代理伺服器的連線
- 在建立房間前檢查目前的代理伺服器設定。
- 測試連線
- 測試中…
- 已連線,耗時 %1$d 毫秒
- 失敗:%1$s
- 預留的代理伺服器 — 請設定你自己的伺服器以自行架設
+ 測試連線
+ 測試中…
+ 已連線,耗時 %1$d 毫秒
+ 失敗:%1$s
連線
- 憑證
- 身分
診斷
@@ -319,9 +305,6 @@
連線中…
房間代碼
ABC123
- 代理伺服器:%1$s
- 請先在「設定」中設定代理伺服器,才能建立或加入房間。
- 代理伺服器設定
已連線
重新連線中…
複製代碼
@@ -341,6 +324,28 @@
無法加入 Jam
請輸入由英文字母和數字組成的六位房間代碼。
用代碼 %1$s 加入我的 Spotube Jam
+ 房間伺服器
+ 伺服器網址
+ 請輸入以 https:// 開頭的網址
+ 建立房間前先檢查房間伺服器。
+ Jam 使用 Spotube 的免費房間伺服器。若要自行架設,請部署 github.com/team-spotube/jam-server 中的伺服器並輸入其網址。
+ 你
+ 伺服器:%1$s
+ 請在設定中指定房間伺服器,以建立或加入房間。
+ 伺服器設定
+ %1$s(你)
+ 你的名稱
+ Jam 裡的其他人會看到你是 %1$s
+ Jam 裡的每個人都看得到。留空即使用 %1$s。新名稱會在你下次建立或加入房間時生效。
+ 最多 %1$d 個字元。
+ 尚未設定 Jam 伺服器
+ 無法連線到 Jam 伺服器。請檢查網路,或 設定 → 群組 Jam 中的伺服器網址。
+ Jam 伺服器的版本不相容。
+ 沒有使用此代碼的房間。請檢查後再試一次。
+ 此房間已滿。
+ 主持人已將你從此房間封鎖。
+ 主持人已將你移出房間。
+ 此房間已在這部裝置上再次開啟,因此這個連線已關閉。
前往專輯
睡眠定時器
@@ -513,14 +518,9 @@
推薦到 Jam
- 尚未設定 Jam 代理伺服器
- 代理伺服器主機為空白
連線失敗
連線中斷
-
- 代理伺服器
- anonymous
已加入 Jam 待播清單
diff --git a/composeApp/src/commonMain/composeResources/values-zh/strings.xml b/composeApp/src/commonMain/composeResources/values-zh/strings.xml
index 774ab6ef..c357b046 100644
--- a/composeApp/src/commonMain/composeResources/values-zh/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-zh/strings.xml
@@ -288,25 +288,11 @@
WebView
在应用内显示网页内容
一起听
- MQTT 代理服务器
- 代理服务器主机
- %1$s:%2$d
- 代理服务器端口
- 使用 TLS
- 用户名(可选)
- 密码(可选)
- 客户端 ID 前缀
- 用于识别此 Spotube 实例
- 加密与代理服务器的连接
- 创建房间前,检查当前的代理服务器设置。
- 测试连接
- 正在测试…
- 已连接,耗时 %1$d 毫秒
- 失败:%1$s
- 占位代理服务器 — 如需自托管,请配置你自己的服务器
+ 测试连接
+ 正在测试…
+ 已连接,耗时 %1$d 毫秒
+ 失败:%1$s
连接
- 凭据
- 身份
诊断
@@ -319,9 +305,6 @@
正在连接…
房间码
ABC123
- 代理服务器:%1$s
- 请先在设置中配置代理服务器,才能创建或加入房间。
- 代理服务器设置
已连接
正在重新连接…
复制房间码
@@ -341,6 +324,28 @@
加入一起听会话失败
请输入由字母和数字组成的六位房间码。
用房间码加入我的 Spotube 一起听:%1$s
+ 房间服务器
+ 服务器网址
+ 请输入以 https:// 开头的网址
+ 创建房间前先检查房间服务器。
+ 一起听使用 Spotube 的免费房间服务器。如需自行托管,请部署 github.com/team-spotube/jam-server 中的服务器并填写其网址。
+ 你
+ 服务器:%1$s
+ 请在设置中指定房间服务器,以创建或加入房间。
+ 服务器设置
+ %1$s(你)
+ 你的名字
+ 房间里的其他人看到你的名字是 %1$s
+ 房间里的所有人都能看到。留空则使用 %1$s。新名字将在你下次创建或加入房间时生效。
+ 最多 %1$d 个字符。
+ 未配置一起听服务器
+ 无法连接到一起听服务器。请检查网络,或在 设置 → 一起听 中检查服务器网址。
+ 一起听服务器的版本不兼容。
+ 没有使用该代码的房间。请检查后重试。
+ 该房间已满。
+ 房主已将你从此房间封禁。
+ 房主已将你移出房间。
+ 此房间已在本设备上再次打开,因此此连接已关闭。
转到专辑
睡眠定时器
@@ -513,14 +518,9 @@
推荐到一起听
- 未配置一起听代理服务器
- 代理服务器主机为空
连接失败
连接已断开
-
- 代理服务器
- anonymous
已添加到一起听队列
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 2fbae5e0..7784db8d 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -310,26 +310,18 @@
WebView
Display web content inside the app
Group Jam
- MQTT Broker
- Broker host
- %1$s:%2$d
- Broker port
- Use TLS
- Username (optional)
- Password (optional)
- Client ID prefix
- Used to identify this Spotube instance
- Encrypt the broker connection
- Check the current broker settings before starting a room.
- Test connection
- Testing…
- Connected in %1$d ms
- Failed: %1$s
- Placeholder broker — configure your own to self-host
+ Room server
+ Server URL
+ Enter a URL starting with https://
+ Check the room server before starting a room.
+ Jam uses Spotube’s free room server. To self-host, deploy the server from github.com/team-spotube/jam-server and enter its URL.
+ Test connection
+ Testing…
+ Connected in %1$d ms
+ Failed: %1$s
Connection
- Credentials
- Identity
Diagnostics
+ You
Group Jam
@@ -341,9 +333,9 @@
Connecting…
Room code
ABC123
- Broker: %1$s
- Set up a broker in Settings to host or join a room.
- Broker settings
+ Server: %1$s
+ Set a room server in Settings to host or join a room.
+ Server settings
Connected
Reconnecting…
Copy code
@@ -352,6 +344,7 @@
Participants (%1$d)
Host
Listener
+ %1$s (you)
Waiting for listeners…
Remove
Ban
@@ -363,6 +356,145 @@
Failed to join jam session
Enter a six-character room code using letters and digits.
Join my Spotube Jam with code: %1$s
+ Your name
+ Others in the jam see you as %1$s
+ Shown to everyone in the jam. Leave it empty to use %1$s. A new name applies the next time you start or join a room.
+ Use at most %1$d characters.
+
+ %1$s %2$s
+
+
+ - Brave
+ - Bright
+ - Calm
+ - Cheerful
+ - Chill
+ - Clever
+ - Cosmic
+ - Cozy
+ - Curious
+ - Dapper
+ - Dreamy
+ - Eager
+ - Electric
+ - Fancy
+ - Fearless
+ - Fizzy
+ - Fluffy
+ - Funky
+ - Gentle
+ - Giddy
+ - Glowing
+ - Golden
+ - Groovy
+ - Happy
+ - Humble
+ - Jazzy
+ - Jolly
+ - Jumpy
+ - Kind
+ - Lively
+ - Lucky
+ - Lunar
+ - Mellow
+ - Merry
+ - Mighty
+ - Misty
+ - Nimble
+ - Peppy
+ - Plucky
+ - Polite
+ - Quick
+ - Quiet
+ - Rapid
+ - Rhythmic
+ - Rosy
+ - Silly
+ - Sleepy
+ - Smooth
+ - Snappy
+ - Soulful
+ - Sparkly
+ - Speedy
+ - Spicy
+ - Sunny
+ - Swift
+ - Tidy
+ - Tiny
+ - Vivid
+ - Wavy
+ - Whistling
+ - Witty
+ - Zany
+ - Zesty
+ - Zippy
+
+
+ - Alpaca
+ - Badger
+ - Beaver
+ - Bison
+ - Bunny
+ - Capybara
+ - Cheetah
+ - Chipmunk
+ - Cobra
+ - Corgi
+ - Coyote
+ - Crane
+ - Dingo
+ - Dolphin
+ - Duckling
+ - Eagle
+ - Falcon
+ - Ferret
+ - Finch
+ - Flamingo
+ - Fox
+ - Gazelle
+ - Gecko
+ - Giraffe
+ - Hedgehog
+ - Heron
+ - Hippo
+ - Ibis
+ - Jaguar
+ - Koala
+ - Lemur
+ - Llama
+ - Lynx
+ - Magpie
+ - Manatee
+ - Marmot
+ - Meerkat
+ - Mongoose
+ - Moose
+ - Narwhal
+ - Ocelot
+ - Octopus
+ - Orca
+ - Otter
+ - Owl
+ - Panda
+ - Pangolin
+ - Parrot
+ - Pelican
+ - Penguin
+ - Puffin
+ - Quokka
+ - Raccoon
+ - Raven
+ - Robin
+ - Seal
+ - Sloth
+ - Sparrow
+ - Squirrel
+ - Tapir
+ - Tiger
+ - Toucan
+ - Walrus
+ - Wombat
+
Go to album
Sleep timer
@@ -549,15 +681,17 @@
Suggest to Jam
- No jam broker configured
- Broker host is empty
+ No jam server configured
+ Couldn’t reach the jam server. Check your network or the server URL in Settings → Jam.
+ The jam server runs an incompatible version.
+ No room with that code. Check it and try again.
+ This room is full.
+ The host banned you from this room.
+ The host removed you from the room.
+ This room was opened again on this device, so this connection was closed.
Connection failed
Connection lost
- Couldn't reach %1$s:%2$d. Check your network or pick another broker in Settings → Jam.
-
- Broker
- anonymous
Added to the jam queue
diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/di/Modules.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/di/Modules.kt
index d7668946..4c45a3fd 100644
--- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/di/Modules.kt
+++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/di/Modules.kt
@@ -86,6 +86,7 @@ import org.spotube.spotube.modules.update.AppUpdateService
import org.spotube.spotube.modules.update.PluginUpdateService
import org.spotube.spotube.modules.settings.SettingsProvider
import org.spotube.spotube.modules.settings.SettingsRepository
+import org.spotube.spotube.modules.settings.UserSettingsStore
import org.spotube.spotube.modules.settings.UserSettingsSource
import org.spotube.spotube.modules.settings.SettingsViewModel
import org.spotube.spotube.modules.settings.JamSettingsViewModel
@@ -151,7 +152,10 @@ val sharedModules = module {
viewModelOf(::MetadataPluginGuardViewModel)
// Settings
- singleOf(::SettingsRepository) { bind() }
+ singleOf(::SettingsRepository) {
+ bind()
+ bind()
+ }
viewModelOf(::SettingsViewModel) { bind() }
viewModelOf(::JamSettingsViewModel)
viewModel {
@@ -283,7 +287,7 @@ val sharedModules = module {
}
single { RemotePlaybackController(get(), get(), get(), get(), get()) }
singleOf(::JamRoomClient)
- single { JamRoomService(get(), get(), get(), get()) }
+ single { JamRoomService(get(), get(), get(), get(), get(), get(), get()) }
singleOf(::AudioPlayerQueueRepository) { bind() }
single {
DeviceAudioPlayerQueue(get(), get(), get(), get(), get())
diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamClock.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamClock.kt
new file mode 100644
index 00000000..770a6834
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamClock.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
+ *
+ * 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 .
+ */
+
+package org.spotube.spotube.core.jam
+
+/**
+ * Estimates the offset between this device's clock and the room server's (NTP-style).
+ *
+ * Each ping is sent at local time `c` and answered with server time `s`; it arrives back
+ * at local time `r`. Assuming symmetric latency, the server read its clock halfway
+ * through, so `offset = s - (c + r) / 2`. Samples with the lowest round trip have the
+ * least room for asymmetry, so the estimate uses the fastest of the recent samples.
+ * Samples older than [maxAgeMs] are dropped, so the estimate follows slow clock drift.
+ */
+class JamClock(private val window: Int = 16, private val maxAgeMs: Long = 15 * 60_000L) {
+ private data class Sample(val rttMs: Long, val offsetMs: Long, val atMs: Long)
+
+ private val samples = ArrayDeque()
+
+ /** Local-to-server offset in ms, or null before the first sample. */
+ var offsetMs: Long? = null
+ private set
+
+ /** Round trip of the sample the offset came from. */
+ var rttMs: Long? = null
+ private set
+
+ fun addSample(clientSentMs: Long, serverMs: Long, clientReceivedMs: Long) {
+ val rtt = clientReceivedMs - clientSentMs
+ if (rtt < 0) return
+ samples.addLast(Sample(rtt, serverMs - (clientSentMs + clientReceivedMs) / 2, clientReceivedMs))
+ while (samples.size > window || samples.first().atMs < clientReceivedMs - maxAgeMs) samples.removeFirst()
+ val best = samples.minBy { it.rttMs }
+ offsetMs = best.offsetMs
+ rttMs = best.rttMs
+ }
+
+ fun serverTime(localMs: Long): Long = localMs + (offsetMs ?: 0L)
+
+ fun reset() {
+ samples.clear()
+ offsetMs = null
+ rttMs = null
+ }
+}
+
+/** What a guest does about the gap between where the room is and where it is. */
+sealed class JamCorrection {
+ /** In sync: play at the room's rate. */
+ data object None : JamCorrection()
+
+ /** Slightly off: speed up or slow down until caught up (inaudible, no gap). */
+ data class Nudge(val rate: Float) : JamCorrection()
+
+ /** Too far off to nudge: jump to [positionMs]. */
+ data class Seek(val positionMs: Long) : JamCorrection()
+}
+
+/**
+ * Turns drift into a [JamCorrection]. Players report their position coarsely (VLC in
+ * ~250 ms steps), so decisions use the median of the last few drift readings, and
+ * small drift inside [deadBandMs] is left alone.
+ */
+class JamDriftCorrector(
+ private val deadBandMs: Long = 120,
+ private val seekThresholdMs: Long = 1_500,
+ /** Drift is closed over roughly this long while nudging. */
+ private val catchUpMs: Long = 3_000,
+ private val maxNudge: Float = 0.05f,
+ private val window: Int = 5,
+ /** Readings needed before a seek, so one noisy report right after a change can't cause one. */
+ private val minReadingsToSeek: Int = 3) {
+ private val drifts = ArrayDeque()
+
+ /**
+ * [expectedMs] is the room's position, [actualMs] ours, [roomRate] the host's rate.
+ * [seekLeadMs] is how long a seek takes to resume here; a seek aims that far ahead.
+ */
+ fun correct(expectedMs: Long, actualMs: Long, roomRate: Float, seekLeadMs: Long): JamCorrection {
+ val drift = expectedMs - actualMs
+ // A huge jump is acted on at once; waiting for the median would replay it for seconds.
+ if (kotlin.math.abs(drift) > seekThresholdMs * 2) {
+ reset()
+ return JamCorrection.Seek(expectedMs + seekLeadMs)
+ }
+ drifts.addLast(drift)
+ while (drifts.size > window) drifts.removeFirst()
+ val smoothed = drifts.sorted()[drifts.size / 2]
+
+ return when {
+ kotlin.math.abs(smoothed) > seekThresholdMs && drifts.size >= minReadingsToSeek -> {
+ reset()
+ JamCorrection.Seek(expectedMs + seekLeadMs)
+ }
+ kotlin.math.abs(smoothed) <= deadBandMs -> JamCorrection.None
+ else -> {
+ val nudge = (smoothed.toFloat() / catchUpMs).coerceIn(-maxNudge, maxNudge)
+ JamCorrection.Nudge(roomRate * (1f + nudge))
+ }
+ }
+ }
+
+ fun reset() = drifts.clear()
+}
diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamDevice.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamDevice.kt
new file mode 100644
index 00000000..ebef9b60
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamDevice.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
+ *
+ * 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 .
+ */
+
+package org.spotube.spotube.core.jam
+
+import okio.ByteString.Companion.encodeUtf8
+
+/**
+ * Stable traits of this device (model, OS version, locale, …) for jam bans. They never
+ * leave the device as they are: [jamDeviceDigest] hashes them with the room code, and the
+ * server hashes that again with the network, so a room only ever holds an opaque value.
+ */
+internal expect fun jamDeviceTraits(): List
+
+/** Per-room digest of [jamDeviceTraits]: unlinkable across rooms. */
+internal fun jamDeviceDigest(roomCode: String): String? = runCatching {
+ val traits = jamDeviceTraits().filter { it.isNotBlank() }
+ if (traits.isEmpty()) return null
+ "spotube-jam-device:$roomCode:${traits.joinToString("|")}".encodeUtf8().sha256().hex()
+}.getOrNull()
+
+/**
+ * This install's participant secret for room [roomCode], derived from the install's
+ * [deviceKey] (random, kept in settings). The same install gets the same identity in a
+ * room every time, so a ban sticks across rejoins; different rooms get unrelated keys.
+ */
+internal fun jamParticipantKey(deviceKey: String, roomCode: String): String =
+ "spotube-jam:$roomCode".encodeUtf8().hmacSha256(deviceKey.encodeUtf8()).hex()
diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamNames.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamNames.kt
new file mode 100644
index 00000000..bc35d56e
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamNames.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
+ *
+ * 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 .
+ */
+
+package org.spotube.spotube.core.jam
+
+import okio.ByteString.Companion.encodeUtf8
+import org.jetbrains.compose.resources.getString
+import org.jetbrains.compose.resources.getStringArray
+import spotube.composeapp.generated.resources.Res
+import spotube.composeapp.generated.resources.jam_name_adjectives
+import spotube.composeapp.generated.resources.jam_name_animals
+import spotube.composeapp.generated.resources.jam_name_format
+
+/** Friendly default names for jam participants ("Mellow Otter"), like npm or Docker use. */
+object JamNames {
+ /** The server's limit (`MAX_NAME_LENGTH` in the jam-server repository's `src/protocol.ts`). */
+ const val MAX_LENGTH = 40
+
+ /**
+ * The generated name for [seed]. The same seed always gets the same name, so with this
+ * install's key a device keeps its name across sessions until the user picks their own.
+ */
+ suspend fun generate(seed: String): String {
+ val adjectives = getStringArray(Res.array.jam_name_adjectives)
+ val animals = getStringArray(Res.array.jam_name_animals)
+ val hash = "spotube-jam-name:$seed".encodeUtf8().sha256()
+ val adjective = adjectives[pick(hash[0], hash[1], adjectives.size)]
+ val animal = animals[pick(hash[2], hash[3], animals.size)]
+ return getString(Res.string.jam_name_format, adjective, animal).take(MAX_LENGTH)
+ }
+
+ /** Cleans a name the user typed; blank means "use the generated one". */
+ fun normalize(input: String): String =
+ input.filterNot { it.isISOControl() }.replace(Regex("\\s+"), " ").trim()
+
+ private fun pick(high: Byte, low: Byte, size: Int): Int =
+ (((high.toInt() and 0xFF) shl 8) or (low.toInt() and 0xFF)) % size
+}
diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamProtocol.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamProtocol.kt
index 03231e66..7744c5f7 100644
--- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamProtocol.kt
+++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamProtocol.kt
@@ -20,91 +20,73 @@ package org.spotube.spotube.core.jam
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import org.spotube.spotube.core.audioplayer.MediaItem
import org.spotube.spotube.core.audioplayer.QueueEntry
+import org.spotube.spotube.core.server.PluginTaggedAudioSource
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
- * Jam messages exchanged over MQTT.
+ * Room payloads. The server only routes and retains them; their shape is owned here.
*
- * - `state` topic: [QueueState] (retained, host -> everyone)
- * - `cmd` topic: [PlaybackCommand], [Kick], [SuggestTrack], [SuggestPlaylist]
- * (anyone -> host, except Kick which is host -> guest)
- * - `presence/{clientId}` topic: [JamPresence] (retained, one per participant)
+ * - [JamQueue] (retained, host -> everyone): the shared queue.
+ * - [JamAnchor] (retained, host -> everyone): where playback is, in server time.
+ * - [JamCommand] (guest -> host): playback control and suggestions.
*/
@Serializable
-sealed class JamMessage {
- @Serializable
- @SerialName("queueState")
- data class QueueState(
- val items: List,
- val currentIndex: Int,
- val shuffleEnabled: Boolean = false,
- /**
- * Whether guests should follow the host's current index. True when the
- * host manually skipped/jumped or loaded a queue; false when the host
- * merely auto-advanced because its song ended (guests stay put).
- */
- val follow: Boolean = false,
- /** Room-wide play/pause; guests follow every change of it. */
- val isPlaying: Boolean = false) : JamMessage()
-
- @Serializable
- @SerialName("playbackCommand")
- data class PlaybackCommand(val command: PlaybackCmd) : JamMessage()
-
- @Serializable
- @SerialName("suggestTrack")
- data class SuggestTrack(
- val mediaItem: JamMediaItem,
- val addedBy: String = "") : JamMessage()
-
- @Serializable
- @SerialName("suggestPlaylist")
- data class SuggestPlaylist(
- val tracks: List,
- val addedBy: String = "") : JamMessage()
-
- @Serializable
- @SerialName("kick")
- data class Kick(
- val participantId: String,
- val reason: String = "kicked") : JamMessage()
-
- @Serializable
- @SerialName("leave")
- data class Leave(val reason: String = "user_left") : JamMessage()
-}
+data class JamQueue(
+ val items: List,
+ val shuffleEnabled: Boolean = false)
/**
- * Playback commands. Queue navigation and play/pause are global — seek,
- * volume, shuffle and loop are local to each participant.
+ * A playback anchor: track [trackKey] (at queue [index]) was at [positionMs] at server
+ * time [serverTimeMs]. While [advancing], everyone extrapolates the position from it at
+ * [rate]; a buffering host sends a non-advancing anchor so guests don't run ahead.
*/
@Serializable
-sealed class PlaybackCmd {
+data class JamAnchor(
+ val trackKey: String,
+ val index: Int,
+ val positionMs: Long,
+ val serverTimeMs: Long,
+ /** Room-wide play/pause intent; guests follow every change of it. */
+ val playing: Boolean,
+ val advancing: Boolean = playing,
+ val rate: Float = 1f,
+ /** The audio source the host plays [trackKey] from, so guests stream the same file. */
+ val source: PluginTaggedAudioSource? = null) {
+ /** The room's position at server time [serverNowMs]. */
+ fun positionAt(serverNowMs: Long): Long =
+ if (advancing) positionMs + ((serverNowMs - serverTimeMs) * rate).toLong() else positionMs
+}
+
+/** Playback control and suggestions. Volume, loop and the local queue view stay local. */
+@Serializable
+sealed class JamCommand {
@Serializable
@SerialName("setPlaying")
- data class SetPlaying(val playing: Boolean) : PlaybackCmd()
+ data class SetPlaying(val playing: Boolean) : JamCommand()
@Serializable
@SerialName("skipNext")
- data object SkipNext : PlaybackCmd()
+ data object SkipNext : JamCommand()
@Serializable
@SerialName("skipPrevious")
- data object SkipPrevious : PlaybackCmd()
+ data object SkipPrevious : JamCommand()
@Serializable
@SerialName("jumpTo")
- data class JumpTo(val index: Int) : PlaybackCmd()
-}
+ data class JumpTo(val index: Int) : JamCommand()
-/** Retained per-participant presence entry (with an MQTT Last Will for leave). */
-@Serializable
-data class JamPresence(
- val clientId: String,
- val displayName: String,
- val isHost: Boolean,
- val left: Boolean = false)
+ @Serializable
+ @SerialName("seekTo")
+ data class SeekTo(val positionMs: Long) : JamCommand()
+
+ @Serializable
+ @SerialName("suggest")
+ data class Suggest(
+ val items: List,
+ val addedBy: String = "") : JamCommand()
+}
@Serializable
data class JamMediaItem(
@@ -117,7 +99,35 @@ data class JamMediaItem(
val coverUrl: String,
val protocol: String,
val addedBy: String = "") {
+ /** Identifies the track across devices: the metadata id, or an opaque id for local files. */
+ val key: String get() = trackId.ifBlank { url }
+
+ /**
+ * This item as received from another participant, made safe to load: streaming tracks
+ * only (a peer's local file can't play here, and a URL would make this device fetch
+ * whatever a peer chose), https covers only, and bounded text. Null if unusable.
+ */
+ fun sanitized(): JamMediaItem? {
+ if (trackId.isBlank() || trackId.length > MAX_ID_LENGTH) return null
+ return copy(
+ url = "",
+ title = title.take(MAX_TEXT_LENGTH),
+ artist = artist.take(MAX_TEXT_LENGTH),
+ album = album.take(MAX_TEXT_LENGTH),
+ durationMs = durationMs.coerceAtLeast(0),
+ coverUrl = coverUrl.takeIf { it.startsWith("https://") && it.length <= MAX_URL_LENGTH }.orEmpty(),
+ protocol = protocol.take(MAX_ID_LENGTH),
+ addedBy = addedBy.take(MAX_ID_LENGTH))
+ }
+
companion object {
+ private const val MAX_ID_LENGTH = 200
+ private const val MAX_TEXT_LENGTH = 300
+ private const val MAX_URL_LENGTH = 2048
+
+ /** The cross-device key of a local file at [path], which doesn't reveal the path. */
+ fun localTrackKey(path: String): String = "local:" + path.hashCode().toUInt().toString(16)
+
fun fromQueueEntry(entry: QueueEntry): JamMediaItem = when (entry) {
is QueueEntry.StreamingTrack -> JamMediaItem(
url = "",
@@ -132,8 +142,9 @@ data class JamMediaItem(
protocol = entry.protocol.name,
addedBy = entry.addedBy)
+ // Only an opaque id: the path would show the host's folders (and user name) to everyone.
is QueueEntry.LocalTrack -> JamMediaItem(
- url = entry.url,
+ url = localTrackKey(entry.url),
trackId = "",
title = entry.name,
artist = entry.artists.joinToString(", "),
diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomClient.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomClient.kt
index 07810202..5be59de1 100644
--- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomClient.kt
+++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomClient.kt
@@ -18,22 +18,31 @@
package org.spotube.spotube.core.jam
import co.touchlab.kermit.Logger
-import com.ditchoom.buffer.Charset
-import com.ditchoom.buffer.codec.asReadBuffer
-import com.ditchoom.buffer.toReadBuffer
-import com.ditchoom.mqtt.client.ConnectionState
-import com.ditchoom.mqtt.client.MqttClient
-import com.ditchoom.mqtt.connection.MqttConnectionOptions
-import com.ditchoom.mqtt.controlpacket.OpaquePublishPayloadCodec
-import com.ditchoom.mqtt.controlpacket.QualityOfService
-import com.ditchoom.mqtt.controlpacket.TopicName
-import com.ditchoom.mqtt.controlpacket.WillConfig
-import com.ditchoom.mqtt5.controlpacket.ConnectionRequest
-import org.spotube.spotube.modules.settings.JamBroker
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.plugins.websocket.WebSockets
+import io.ktor.client.plugins.websocket.webSocketSession
+import io.ktor.client.request.get
+import io.ktor.client.statement.bodyAsText
+import io.ktor.http.URLBuilder
+import io.ktor.http.URLProtocol
+import io.ktor.http.appendPathSegments
+import io.ktor.http.isSuccess
+import io.ktor.websocket.DefaultWebSocketSession
+import io.ktor.websocket.Frame
+import io.ktor.websocket.close
+import io.ktor.websocket.readText
+import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -41,47 +50,81 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonClassDiscriminator
import org.jetbrains.compose.resources.getString
+import org.spotube.spotube.core.network.HttpClientFactory
import spotube.composeapp.generated.resources.Res
-import spotube.composeapp.generated.resources.jam_error_broker_host_empty
-import spotube.composeapp.generated.resources.jam_error_broker_unreachable
+import spotube.composeapp.generated.resources.jam_error_banned
import spotube.composeapp.generated.resources.jam_error_connection_failed
import spotube.composeapp.generated.resources.jam_error_connection_lost
-import spotube.composeapp.generated.resources.jam_error_no_broker
-import spotube.composeapp.generated.resources.settings_jam_broker_test_ok
-import org.koin.core.component.KoinComponent
-import kotlin.time.Duration.Companion.seconds
+import spotube.composeapp.generated.resources.jam_error_kicked
+import spotube.composeapp.generated.resources.jam_error_replaced
+import spotube.composeapp.generated.resources.jam_error_room_full
+import spotube.composeapp.generated.resources.jam_error_room_not_found
+import spotube.composeapp.generated.resources.jam_error_server_incompatible
+import spotube.composeapp.generated.resources.jam_error_server_unreachable
+import spotube.composeapp.generated.resources.settings_jam_server_test_ok
+import kotlin.time.Clock
/**
- * Thin facade over the Ditchoom MQTT 5 client for one jam room.
+ * WebSocket client for one jam room on the room server (https://github.com/team-spotube/jam-server, a Cloudflare
+ * Durable Object per room code).
*
- * Topics (room code `C`):
- * - `spotube/jam/{C}/state` retained — [JamMessage.QueueState] (host -> everyone)
- * - `spotube/jam/{C}/cmd` volatile — commands/suggestions (everyone -> host, host -> guest)
- * - `spotube/jam/{C}/presence/{clientId}` retained — [JamPresence], with a Last Will
- * (`left = true`) so a dropped client disappears from the room automatically.
+ * The server keeps the host's latest queue and anchor for late joiners, routes guest
+ * commands to the host and is the clock every participant syncs playback against
+ * ([clock]). A dropped connection is retried with the same client id, which the
+ * server treats as the same participant (a host keeps the room for a grace period).
*
- * The library keeps the connection alive (auto-reconnect + backoff); this class only
- * re-establishes the room subscription and re-publishes presence after a reconnect.
+ * The room is billed per incoming message, and on phones every transmission wakes the
+ * cellular radio for seconds, so the steady state sends almost nothing: one keep-alive
+ * a minute (answered by the server runtime for free) and a clock re-sync every
+ * [CLOCK_RESYNC_INTERVAL_MS]. Reconnect attempts back off to [RECONNECT_BACKOFF_MS]'s
+ * last step while the network stays away.
*/
-class JamRoomClient : KoinComponent {
+class JamRoomClient(httpClientFactory: HttpClientFactory) {
private val log = Logger.withTag("JamRoomClient")
- private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+ // Nothing here may take the app down: a network failure is a reconnect, not a crash.
+ private val scope = CoroutineScope(
+ SupervisorJob() + Dispatchers.IO +
+ CoroutineExceptionHandler { _, e -> log.w(e) { "Unexpected jam connection failure" } })
private val json = Json {
ignoreUnknownKeys = true
classDiscriminator = "type"
encodeDefaults = true
}
+ private val http = httpClientFactory.create {
+ install(WebSockets)
+ install(HttpTimeout) { connectTimeoutMillis = CONNECT_TIMEOUT_MS }
+ }
+ private val plainHttp = httpClientFactory.create {
+ install(HttpTimeout) { requestTimeoutMillis = CONNECT_TIMEOUT_MS }
+ }
- private var client: MqttClient? = null
+ val clock = JamClock()
- private var roomCode: String? = null
- private var localPresence: JamPresence? = null
+ private var session: DefaultWebSocketSession? = null
+ private var connectionJob: Job? = null
+ private var reader: Deferred? = null
+ private var pingId = 0
+ /** When the current socket last received anything; a silent socket is a dead one. */
+ private var lastFrameAtMs = 0L
+ /** The room's host as last reported by the server. */
+ private var hostId: String? = null
+
+ /**
+ * Our public participant id in the current room, as the server derived it from our
+ * secret key. Null until the first welcome.
+ */
+ var selfId: String? = null
+ private set
+ private val pongs = MutableStateFlow(0)
private val _isConnected = MutableStateFlow(false)
val isConnected: StateFlow = _isConnected.asStateFlow()
@@ -89,244 +132,429 @@ class JamRoomClient : KoinComponent {
private val _connectionError = MutableStateFlow(null)
val connectionError: StateFlow = _connectionError.asStateFlow()
- private val _state = MutableSharedFlow(replay = 1, extraBufferCapacity = 8)
- val state: SharedFlow = _state.asSharedFlow()
-
- private val _commands = MutableSharedFlow(extraBufferCapacity = 32)
- val commands: SharedFlow = _commands.asSharedFlow()
-
- private val _presence = MutableStateFlow