multi player / per user playing and translation

This commit is contained in:
2026-08-28 16:53:05 +02:00
parent 2743dfb4c0
commit e10f2cf7d7
17 changed files with 1432 additions and 482 deletions
+10 -4
View File
@@ -293,23 +293,29 @@
(with-lock playback (lambda () (poll/locked! playback))))
(loop)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Create playlist-aware playback for one network renderer.
; pre : Device is a media renderer, callbacks are procedures, and
; media-server is a running shared media-file-server.
; post : A DLNA player and its state-monitor thread are running.
; result : A playback adapter that publishes through the supplied server.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (make-dlna-playback device
tracks
update
error
#:port [port 8734]
#:media-file-server media-server
#:poll-seconds [poll-seconds 1])
(define raw
(make-dlna-player device
#:port port
#:path "/rkt-web-player/"))
#:media-file-server media-server))
(define playback
(dlna-playback raw tracks update error 'off #f #f #f
#f #f #f #f 0 #f #t #t #f
(make-semaphore 1)))
(set-dlna-playback-monitor!
playback
(thread (lambda () (monitor-loop playback poll-seconds))))
(thread (λ () (monitor-loop playback poll-seconds))))
playback)
(define (dlna-playback-play-index! playback index)
+7 -11
View File
@@ -7,7 +7,8 @@
racket/path
racket/port
racket/string
simple-log)
simple-log
"translate.rkt")
(provide (struct-out player-agent-runtime)
make-player-agent-runtime)
@@ -321,17 +322,12 @@
(with-handlers
((exn:fail:agent-denied?
(lambda (exception)
(define message
(string-append
"Deze playback agent is niet toegelaten door de server. "
"Voeg het volgende applicatie-ID toe aan [playback-agents] "
"in de server-INI:\n\n"
app-id))
(define message (format (tr 'denied-message) app-id))
(warn-player-agent "Agent authorization refused: ~a"
(exn-message exception))
(set-agent-error! message)
(status-callback
"Niet geautoriseerd — applicatie-ID staat niet in de server-INI")
(tr 'unauthorized-status))
(unless authorization-notified?
(set! authorization-notified? #t)
(denied-callback message))
@@ -344,7 +340,7 @@
(exn-message exception))
(set-agent-error! (exn-message exception))
(status-callback
(format "Niet verbonden: ~a" (exn-message exception)))
(format (tr 'disconnected) (exn-message exception)))
(when running
(sleep 3)
(poll-loop)))))
@@ -352,7 +348,7 @@
"/api/agent/register"
(hasheq 'appId app-id 'name assigned-name))
(clear-agent-error!)
(status-callback "Verbonden")
(status-callback (tr 'connected))
(info-player-agent "Registered at ~a as ~a" server-url assigned-name)
(let loop ()
(when running
@@ -390,7 +386,7 @@
(define (start!)
(unless running
(set! running #t)
(status-callback "Verbinden…")
(status-callback (tr 'connecting))
(set! worker (thread poll-loop))))
(define (stop!)
+22 -19
View File
@@ -9,7 +9,8 @@
simple-log
"player-agent-config.rkt"
"player-agent-core.rkt"
"player-agent-tray.rkt")
"player-agent-tray.rkt"
"translate.rkt")
(provide run-player-agent-gui)
@@ -69,7 +70,7 @@
(define (show-denial! message)
(queue-callback
(lambda ()
(message-box "Playback agent niet toegestaan"
(message-box (tr 'denied-title)
message
frame
'(ok stop)))
@@ -97,13 +98,13 @@
((and artist (not (string=? artist "")) title)
(format "~a — ~a" artist title))
(title title)
(else "Geen track geselecteerd")))
(else (tr 'no-track-selected))))
(define prefix
(cond
((string=? state "playing") "Speelt")
((string=? state "paused") "Gepauzeerd")
((string=? state "starting") "Laden")
((string=? state "stopped") "Gestopt")
((string=? state "playing") (tr 'playing))
((string=? state "paused") (tr 'paused))
((string=? state "starting") (tr 'loading))
((string=? state "stopped") (tr 'stopped))
(else state)))
(define position (hash-ref snapshot 'position 0))
(define duration (hash-ref snapshot 'duration 'null))
@@ -124,7 +125,7 @@
"")
(if (number? channels)
(format "~a ~a" channels
(if (= channels 1) "kanaal" "kanalen"))
(tr (if (= channels 1) 'channel 'channels)))
"")
(if (and (string? format-name) (not (string=? format-name "")))
format-name
@@ -138,7 +139,7 @@
(format " #~a" track-number)
"")
track-label)
"Er wordt niets afgespeeld"))
(tr 'no-track)))
(send playback-details set-label (string-join details " · "))
(send playback-filename
set-label
@@ -160,7 +161,7 @@
(save-player-agent-config! config)
(send name-field set-value next-name)
((player-agent-runtime-reconnect! runtime) next-server next-name)
(send connect-button set-label "Opnieuw verbinden"))
(send connect-button set-label (tr 'reconnect)))
(define (shutdown!)
(unless shutting-down?
@@ -191,7 +192,7 @@
(set! frame
(new agent-frame%
(label "RKT Web Player Agent")
(label (tr 'app-title))
(width 560)
(height 310)))
(define panel
@@ -199,13 +200,15 @@
(parent frame)
(alignment '(left top))))
(set! server-field
(input-field "RKT Web Player server"
(input-field (tr 'server)
(player-agent-config-server-url config)
panel))
(set! name-field
(input-field "Naam" (player-agent-config-name config) panel))
(input-field (tr 'name) (player-agent-config-name config) panel))
(define id-field
(input-field "Applicatie-ID" (player-agent-config-app-id config) panel))
(input-field (tr 'application-id)
(player-agent-config-app-id config)
panel))
;; Lock the editor, not the native widget. Disabled Windows controls render
;; their label and text poorly on some display configurations.
(send (send id-field get-editor) lock #t)
@@ -213,13 +216,13 @@
(define playback-panel
(new group-box-panel%
(parent panel)
(label "Afspelen")
(label (tr 'playback))
(alignment '(left top))
(stretchable-height #f)))
(set! playback-message
(new message%
(parent playback-panel)
(label "Er wordt niets afgespeeld")
(label (tr 'no-track))
(auto-resize #t)))
(set! playback-details
(new message%
@@ -239,12 +242,12 @@
(set! connect-button
(new button%
(parent controls)
(label "Opslaan en verbinden")
(label (tr 'save-connect))
(callback (lambda (_button _event) (reconnect!)))))
(set! status-message
(new message%
(parent controls)
(label "Verbinden…")
(label (tr 'connecting))
(auto-resize #t)))
(set! playback-timer
@@ -268,5 +271,5 @@
(send frame show #t)
((player-agent-runtime-start! runtime))
(send connect-button set-label "Opnieuw verbinden")
(send connect-button set-label (tr 'reconnect))
frame)
+5 -3
View File
@@ -1,5 +1,7 @@
#lang racket/base
(require "translate.rkt")
;; SDL3 is deliberately loaded dynamically. The GUI agent keeps working
;; without the optional Racket package and native SDL3 runtime.
@@ -20,11 +22,11 @@
(define update-trays! (dynamic-require 'sdl3 'update-trays!))
(define tray-destroy! (dynamic-require 'sdl3 'tray-destroy!))
(sdl-init! '(video events))
(define tray (make-tray #f "RKT Web Player Agent"))
(define tray (make-tray #f (tr 'app-title)))
(define menu (make-tray-menu tray))
(define show-entry (insert-tray-entry! menu "RKT Web Player Agent openen"))
(define show-entry (insert-tray-entry! menu (tr 'tray-open)))
(insert-tray-entry! menu #f)
(define quit-entry (insert-tray-entry! menu "Afsluiten"))
(define quit-entry (insert-tray-entry! menu (tr 'quit)))
(set-tray-entry-callback! show-entry (lambda (_) (show-window!)))
(set-tray-entry-callback! quit-entry (lambda (_) (quit!)))
(tray-controller
+611 -328
View File
File diff suppressed because it is too large Load Diff
+52 -1
View File
@@ -11,7 +11,9 @@
open-playlist-store
close-playlist-store!
load-user-playlists
save-user-playlists!)
save-user-playlists!
load-user-language
save-user-language!)
(struct persisted-tab (id name tracks) #:transparent)
(struct playlist-store (keystore lock) #:transparent)
@@ -19,6 +21,11 @@
(define (user-playlists-key username)
(format "playlists-for-~a" username))
(define (user-language-key username)
(format "language-for-~a" username))
(define supported-language-names '("en" "nl" "de" "fr" "es"))
(define (track->datum item)
(hasheq 'file (path->string (track-file item))
'title (track-title item)
@@ -116,6 +123,45 @@
(ks-set! ks index-key ids))
(void)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Load one user's persisted interface language.
; pre : Store is #f or an open playlist store; username is normalized.
; post : Store contents remain unchanged.
; result : A supported ISO language name, or #f when none was saved.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (load-user-language store username)
(and store
(call-with-semaphore
(playlist-store-lock store)
(λ ()
(define value
(ks-get (playlist-store-keystore store)
(user-language-key username)
#f))
(and (member value supported-language-names) value)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Persist one user's interface language.
; pre : Store is #f or open, username is normalized, and language is one of
; en, nl, de, fr, or es.
; post : The user's language key contains language when a store exists.
; result : Void.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (save-user-language! store username language)
(unless (member language supported-language-names)
(raise-argument-error
'save-user-language!
"one of en, nl, de, fr, or es"
language))
(when store
(call-with-semaphore
(playlist-store-lock store)
(λ ()
(ks-set! (playlist-store-keystore store)
(user-language-key username)
language))))
(void))
(module+ test
(require rackunit
uuid/random)
@@ -166,6 +212,11 @@
(check-equal?
(map persisted-tab-name (load-user-playlists store "local" libraries))
'("Local"))
(check-false (load-user-language store "hans"))
(save-user-language! store "hans" "fr")
(check-equal? (load-user-language store "hans") "fr")
(check-exn exn:fail:contract?
(λ () (save-user-language! store "hans" "it")))
;; Rewriting the user's GUID index durably removes the omitted playlist.
(save-user-playlists!
+20
View File
@@ -119,6 +119,24 @@
(request-jsexpr request)
#:username (request-username request)))))
(define (preferences-handler request)
(json-response
(hasheq
'language
(or (player-user-language
current-player
#:username (request-username request))
'null))))
(define (preferences-update-handler request)
(with-handlers ((exn:fail? error-response))
(define language (hash-ref (request-jsexpr request) 'language #f))
(player-user-language!
current-player
language
#:username (request-username request))
(json-response (hasheq 'language language))))
(define (agent-register-handler request)
(with-handlers
((exn:fail:agent-denied? agent-error-response)
@@ -190,6 +208,8 @@
[("api" "auth" "logout") #:method "post" auth-logout-handler]
[("api" "state") #:method "get" state-handler]
[("api" "discover") #:method "post" discover-handler]
[("api" "preferences") #:method "get" preferences-handler]
[("api" "preferences") #:method "post" preferences-update-handler]
[("api" "agent" "register") #:method "post" agent-register-handler]
[("api" "agent" "poll") #:method "post" agent-poll-handler]
[("api" "agent" "media" (string-arg) (string-arg))
+217
View File
@@ -0,0 +1,217 @@
#lang racket/base
(require racket/string)
(provide tr
__
languages
set-lang!
current-lang)
(define translation-map
(hasheq
'en
(hasheq
'app-title "RKT Web Player Agent"
'server "RKT Web Player server"
'name "Name"
'application-id "Application ID"
'playback "Playback"
'no-track "Nothing is playing"
'no-track-selected "No track selected"
'save-connect "Save and connect"
'reconnect "Reconnect"
'connecting "Connecting…"
'connected "Connected"
'denied-title "Playback agent not allowed"
'denied-message "This playback agent is not allowed by the server. Add the following application ID to [playback-agents] in the server INI:\n\n~a"
'unauthorized-status "Not authorized — application ID is not in the server INI"
'disconnected "Not connected: ~a"
'playing "Playing"
'paused "Paused"
'loading "Loading"
'stopped "Stopped"
'channel "channel"
'channels "channels"
'tray-open "Open RKT Web Player Agent"
'quit "Quit")
'nl
(hasheq
'app-title "RKT Web Player Agent"
'server "RKT Web Player server"
'name "Naam"
'application-id "Applicatie-ID"
'playback "Afspelen"
'no-track "Er wordt niets afgespeeld"
'no-track-selected "Geen track geselecteerd"
'save-connect "Opslaan en verbinden"
'reconnect "Opnieuw verbinden"
'connecting "Verbinden…"
'connected "Verbonden"
'denied-title "Playback agent niet toegestaan"
'denied-message "Deze playback agent is niet toegelaten door de server. Voeg het volgende applicatie-ID toe aan [playback-agents] in de server-INI:\n\n~a"
'unauthorized-status "Niet geautoriseerd — applicatie-ID staat niet in de server-INI"
'disconnected "Niet verbonden: ~a"
'playing "Speelt"
'paused "Gepauzeerd"
'loading "Laden"
'stopped "Gestopt"
'channel "kanaal"
'channels "kanalen"
'tray-open "RKT Web Player Agent openen"
'quit "Afsluiten")
'de
(hasheq
'app-title "RKT Web Player Agent"
'server "RKT Web Player Server"
'name "Name"
'application-id "Anwendungs-ID"
'playback "Wiedergabe"
'no-track "Keine Wiedergabe"
'no-track-selected "Kein Titel ausgewählt"
'save-connect "Speichern und verbinden"
'reconnect "Neu verbinden"
'connecting "Verbinden…"
'connected "Verbunden"
'denied-title "Playback-Agent nicht zugelassen"
'denied-message "Dieser Playback-Agent ist vom Server nicht zugelassen. Fügen Sie die folgende Anwendungs-ID unter [playback-agents] in die Server-INI ein:\n\n~a"
'unauthorized-status "Nicht autorisiert — Anwendungs-ID fehlt in der Server-INI"
'disconnected "Nicht verbunden: ~a"
'playing "Wiedergabe"
'paused "Pausiert"
'loading "Laden"
'stopped "Gestoppt"
'channel "Kanal"
'channels "Kanäle"
'tray-open "RKT Web Player Agent öffnen"
'quit "Beenden")
'fr
(hasheq
'app-title "Agent RKT Web Player"
'server "Serveur RKT Web Player"
'name "Nom"
'application-id "ID dapplication"
'playback "Lecture"
'no-track "Aucune lecture en cours"
'no-track-selected "Aucune piste sélectionnée"
'save-connect "Enregistrer et connecter"
'reconnect "Reconnecter"
'connecting "Connexion…"
'connected "Connecté"
'denied-title "Agent de lecture non autorisé"
'denied-message "Cet agent de lecture nest pas autorisé par le serveur. Ajoutez lID dapplication suivant à [playback-agents] dans le fichier INI du serveur :\n\n~a"
'unauthorized-status "Non autorisé — lID dapplication est absent du fichier INI du serveur"
'disconnected "Non connecté : ~a"
'playing "Lecture"
'paused "En pause"
'loading "Chargement"
'stopped "Arrêté"
'channel "canal"
'channels "canaux"
'tray-open "Ouvrir lagent RKT Web Player"
'quit "Quitter")
'es
(hasheq
'app-title "Agente de RKT Web Player"
'server "Servidor RKT Web Player"
'name "Nombre"
'application-id "ID de aplicación"
'playback "Reproducción"
'no-track "No se está reproduciendo nada"
'no-track-selected "No hay ninguna pista seleccionada"
'save-connect "Guardar y conectar"
'reconnect "Volver a conectar"
'connecting "Conectando…"
'connected "Conectado"
'denied-title "Agente de reproducción no permitido"
'denied-message "El servidor no permite este agente de reproducción. Añade el siguiente ID de aplicación a [playback-agents] en el INI del servidor:\n\n~a"
'unauthorized-status "No autorizado — el ID de aplicación no está en el INI del servidor"
'disconnected "Sin conexión: ~a"
'playing "Reproduciendo"
'paused "En pausa"
'loading "Cargando"
'stopped "Detenido"
'channel "canal"
'channels "canales"
'tray-open "Abrir el agente de RKT Web Player"
'quit "Salir")))
(define (system-language)
(define language-name
(string-downcase (format "~a" (system-language+country))))
(define short-name (car (regexp-split #px"[-_]" language-name)))
(define candidate (string->symbol short-name))
(if (hash-has-key? translation-map candidate) candidate 'en))
(define language (system-language))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Return the supported language symbols and their native names.
; pre : None.
; post : Translation state remains unchanged.
; result : An association list suitable for a language selector.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (languages)
'((en "English")
(nl "Nederlands")
(de "Deutsch")
(fr "Français")
(es "Español")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Select the language used by tr and __.
; pre : Value is one of the symbols returned by languages.
; post : Subsequent translations use value.
; result : Void.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (set-lang! value)
(unless (hash-has-key? translation-map value)
(raise-argument-error 'set-lang! "supported language symbol" value))
(set! language value))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Report the active translation language.
; pre : None.
; post : Translation state remains unchanged.
; result : A supported language symbol.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (current-lang)
language)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Translate an application string identifier.
; pre : Id is a symbol.
; post : Translation state remains unchanged.
; result : The active translation, its English fallback, or the identifier.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (tr id)
(hash-ref (hash-ref translation-map language)
id
(λ ()
(hash-ref (hash-ref translation-map 'en)
id
(λ () (symbol->string id))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Provide the conventional short alias used by rktplayer GUI code.
; pre : Id is a symbol.
; post : Translation state remains unchanged.
; result : The same translated string as tr.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (__ id)
(tr id))
(module+ test
(require rackunit)
(define original-language (current-lang))
(dynamic-wind
void
(λ ()
(set-lang! 'nl)
(check-equal? (tr 'connected) "Verbonden")
(set-lang! 'de)
(check-equal? (__ 'quit) "Beenden")
(check-equal? (tr 'unknown-translation) "unknown-translation")
(check-exn exn:fail:contract? (λ () (set-lang! 'xx))))
(λ () (set-lang! original-language))))