ok
This commit is contained in:
+301
-59
@@ -50,11 +50,13 @@
|
||||
#:transparent)
|
||||
|
||||
(struct playlist-tab
|
||||
(id [name #:mutable] [tracks #:mutable])
|
||||
(id [name #:mutable] [tracks #:mutable] [snapshot #:auto #:mutable])
|
||||
#:auto-value #f
|
||||
#:transparent)
|
||||
|
||||
(struct playlist-context
|
||||
([tabs #:mutable] [current-index #:mutable])
|
||||
([tabs #:mutable] [current-index #:mutable] [saved #:auto #:mutable])
|
||||
#:auto-value '()
|
||||
#:transparent)
|
||||
|
||||
(struct playback-session
|
||||
@@ -260,36 +262,45 @@
|
||||
(list-ref (player-tabs value)
|
||||
(player-current-tab-index value)))
|
||||
|
||||
;;; Invalidate the browser snapshot only when the immutable track list changes.
|
||||
(define (save-current-tab! value)
|
||||
(set-playlist-tab-tracks!
|
||||
(current-tab value)
|
||||
(player-tracks value)))
|
||||
(let ((tab (current-tab value))
|
||||
(tracks (player-tracks value)))
|
||||
(unless (eq? tracks (playlist-tab-tracks tab))
|
||||
(set-playlist-tab-tracks! tab tracks)
|
||||
(set-playlist-tab-snapshot! tab #f))))
|
||||
|
||||
(define (normal-playlist-username username)
|
||||
(let ((value (and (string? username)
|
||||
(string-downcase (string-trim username)))))
|
||||
(if (and value (not (string=? value ""))) value "anonymous")))
|
||||
|
||||
(define (new-playlist-context value username)
|
||||
(define stored
|
||||
(load-user-playlists (player-playlist-store value)
|
||||
username
|
||||
(player-libraries value)))
|
||||
(playlist-context
|
||||
(if (pair? stored)
|
||||
(for/list ((tab (in-list stored)))
|
||||
(playlist-tab (persisted-tab-id tab)
|
||||
(persisted-tab-name tab)
|
||||
(persisted-tab-tracks tab)))
|
||||
(list (playlist-tab (uuid-string) "Default" '())))
|
||||
0))
|
||||
;;; Restore open tabs and the library using the same object for a shared UUID.
|
||||
;;; Old stores have no saved index, so their tabs remain open and unsaved.
|
||||
(define (restore-playlist-context store username libraries)
|
||||
(let* ((restore (λ (tab) (playlist-tab (persisted-tab-id tab)
|
||||
(persisted-tab-name tab)
|
||||
(persisted-tab-tracks tab))))
|
||||
(opened (map restore (load-user-playlists store username libraries)))
|
||||
(tabs (if (pair? opened) opened
|
||||
(list (playlist-tab (uuid-string) "Default" '()))))
|
||||
(context (playlist-context tabs 0)))
|
||||
(set-playlist-context-saved!
|
||||
context
|
||||
(map (λ (stored)
|
||||
(or (findf (λ (tab) (string=? (playlist-tab-id tab) (persisted-tab-id stored)))
|
||||
tabs)
|
||||
(restore stored)))
|
||||
(load-user-playlists store username libraries #:saved? #t)))
|
||||
context))
|
||||
|
||||
(define (playlist-context-for! value username)
|
||||
(define normalized (normal-playlist-username username))
|
||||
(hash-ref!
|
||||
(player-playlist-contexts value)
|
||||
normalized
|
||||
(λ () (new-playlist-context value normalized))))
|
||||
(λ () (restore-playlist-context (player-playlist-store value)
|
||||
normalized (player-libraries value)))))
|
||||
|
||||
(define (context-tracks context)
|
||||
(playlist-tab-tracks
|
||||
@@ -361,7 +372,12 @@
|
||||
(for/list ((tab (in-list (player-tabs value))))
|
||||
(persisted-tab (playlist-tab-id tab)
|
||||
(playlist-tab-name tab)
|
||||
(playlist-tab-tracks tab)))))
|
||||
(playlist-tab-tracks tab)))
|
||||
#:saved
|
||||
(map (λ (tab) (persisted-tab (playlist-tab-id tab)
|
||||
(playlist-tab-name tab)
|
||||
(playlist-tab-tracks tab)))
|
||||
(playlist-context-saved context))))
|
||||
|
||||
(define (normalize-state state)
|
||||
(cond
|
||||
@@ -927,25 +943,28 @@
|
||||
trimmed)
|
||||
(persist-playlists! value)))))
|
||||
|
||||
;;; Close a tab, retaining saved playlists in the user's library.
|
||||
(define (delete-tab! value session index)
|
||||
(when (= (length (player-tabs value)) 1)
|
||||
(raise-arguments-error
|
||||
'player-command!
|
||||
"the last playlist tab cannot be removed"))
|
||||
(unless (and (exact-nonnegative-integer? index)
|
||||
(< index (length (player-tabs value))))
|
||||
(raise-arguments-error
|
||||
'player-command!
|
||||
"playlist tab does not exist"
|
||||
"index" index))
|
||||
(let ((context (playlist-context-for! value (player-active-playlist-user value))))
|
||||
(when (and (= (length (player-tabs value)) 1)
|
||||
(not (memq (current-tab value) (playlist-context-saved context))))
|
||||
(raise-arguments-error 'player-command! "the last playlist tab cannot be removed")))
|
||||
(stop-playback! value session)
|
||||
(with-state-lock
|
||||
value
|
||||
(λ ()
|
||||
(save-current-tab! value)
|
||||
(let* ((tabs
|
||||
(let* ((remaining
|
||||
(append (take (player-tabs value) index)
|
||||
(drop (player-tabs value) (+ index 1))))
|
||||
(tabs (if (pair? remaining) remaining
|
||||
(list (playlist-tab (uuid-string) "Default" '()))))
|
||||
(new-index
|
||||
(min (player-current-tab-index value)
|
||||
(- (length tabs) 1))))
|
||||
@@ -958,6 +977,44 @@
|
||||
(set-playback-session-current-index! session #f)
|
||||
(persist-playlists! value)))))
|
||||
|
||||
;;; Save an open tab in the library once; subsequent tab edits share its identity.
|
||||
(define (save-playlist! value id name)
|
||||
(let* ((context (playlist-context-for! value (player-active-playlist-user value)))
|
||||
(tab (findf (λ (tab) (equal? (playlist-tab-id tab) id)) (player-tabs value)))
|
||||
(trimmed (string-trim name)))
|
||||
(unless tab
|
||||
(raise-arguments-error 'player-command! "playlist tab does not exist" "id" id))
|
||||
(when (string=? trimmed "")
|
||||
(raise-arguments-error 'player-command! "playlist name cannot be empty"))
|
||||
(with-state-lock
|
||||
value
|
||||
(λ ()
|
||||
(save-current-tab! value)
|
||||
(set-playlist-tab-name! tab trimmed)
|
||||
(unless (memq tab (playlist-context-saved context))
|
||||
(set-playlist-context-saved! context
|
||||
(append (playlist-context-saved context) (list tab))))
|
||||
(persist-playlists! value)))))
|
||||
|
||||
;;; Reveal a saved playlist as its own tab, reusing an existing tab by identity.
|
||||
;;; Opening never copies tracks into another tab; play? starts this playlist.
|
||||
(define (open-playlist! value session id play?)
|
||||
(let* ((context (playlist-context-for! value (player-active-playlist-user value)))
|
||||
(tab (findf (λ (tab) (equal? (playlist-tab-id tab) id))
|
||||
(playlist-context-saved context))))
|
||||
(unless tab
|
||||
(raise-arguments-error 'player-command! "saved playlist does not exist" "id" id))
|
||||
(let ((index (index-of (player-tabs value) tab eq?)))
|
||||
(unless index
|
||||
(with-state-lock
|
||||
value
|
||||
(λ ()
|
||||
(save-current-tab! value)
|
||||
(set-player-tabs! value (append (player-tabs value) (list tab))))))
|
||||
(select-tab! value session (or index (- (length (player-tabs value)) 1)))))
|
||||
(when (and play? (pair? (player-tracks value)))
|
||||
(play-index! value session 0)))
|
||||
|
||||
(define (track->jsexpr item index)
|
||||
(hasheq 'index index
|
||||
'title (track-title item)
|
||||
@@ -984,14 +1041,29 @@
|
||||
'name (browser-entry-name entry)
|
||||
'kind (symbol->string (browser-entry-kind entry))))
|
||||
|
||||
;;; Include the sum of known track durations in each tab's browser summary.
|
||||
(define (tab->jsexpr tab index)
|
||||
(hasheq 'index index
|
||||
'id (playlist-tab-id tab)
|
||||
'name (playlist-tab-name tab)
|
||||
'count (length (playlist-tab-tracks tab))
|
||||
'duration (apply + (map (λ (item) (or (track-duration item) 0))
|
||||
(playlist-tab-tracks tab)))))
|
||||
;;; Cache serialized tracks and their summary until save-current-tab! invalidates
|
||||
;;; them. A fresh opaque version also prevents cache reuse across server restarts.
|
||||
(define (tab-snapshot tab)
|
||||
(or (playlist-tab-snapshot tab)
|
||||
(let* ((tracks (playlist-tab-tracks tab))
|
||||
(snapshot
|
||||
(hasheq 'version (uuid-string)
|
||||
'count (length tracks)
|
||||
'duration (apply + (map (λ (item) (or (track-duration item) 0))
|
||||
tracks))
|
||||
'tracks (map track->jsexpr tracks (range (length tracks))))))
|
||||
(set-playlist-tab-snapshot! tab snapshot)
|
||||
snapshot)))
|
||||
|
||||
;;; Return a small tab summary without rebuilding or transferring its tracks.
|
||||
(define (tab->jsexpr tab index [saved? #f])
|
||||
(let ((snapshot (tab-snapshot tab)))
|
||||
(hasheq 'index index
|
||||
'id (playlist-tab-id tab)
|
||||
'name (playlist-tab-name tab)
|
||||
'saved saved?
|
||||
'count (hash-ref snapshot 'count)
|
||||
'duration (hash-ref snapshot 'duration))))
|
||||
|
||||
(define (normal-device-id device)
|
||||
(let ((id (upnp-device-udn device)))
|
||||
@@ -1121,7 +1193,12 @@
|
||||
(rename-tab! value index
|
||||
(or (json-string data 'name #f) "")))
|
||||
((string=? command "tab-delete")
|
||||
(delete-tab! value session index)))))
|
||||
(delete-tab! value session index))
|
||||
((string=? command "playlist-save")
|
||||
(save-playlist! value (json-string data 'id #f) (or (json-string data 'name #f) "")))
|
||||
((member command '("playlist-open" "playlist-play"))
|
||||
(open-playlist! value session (json-string data 'id #f)
|
||||
(string=? command "playlist-play"))))))
|
||||
|
||||
(define (perform-command! value session command data)
|
||||
(cond
|
||||
@@ -1131,7 +1208,7 @@
|
||||
((member command '("track-remove" "track-move"
|
||||
"playlist-clear" "tab-add"
|
||||
"tab-select" "tab-rename"
|
||||
"tab-delete"))
|
||||
"tab-delete" "playlist-save" "playlist-open" "playlist-play"))
|
||||
(perform-playlist-command! value session command data))
|
||||
((string=? command "play")
|
||||
(play-index!
|
||||
@@ -1278,23 +1355,15 @@
|
||||
app-id))
|
||||
(string-downcase app-id)))
|
||||
(define store (open-playlist-store playlist-keystore))
|
||||
(define stored-tabs
|
||||
(load-user-playlists store "anonymous" libraries))
|
||||
(let* ((library (and (pair? libraries) (car libraries)))
|
||||
(browser-entries
|
||||
(if library
|
||||
(browse-library library '())
|
||||
'()))
|
||||
(tabs
|
||||
(if (pair? stored-tabs)
|
||||
(for/list ((tab (in-list stored-tabs)))
|
||||
(playlist-tab (persisted-tab-id tab)
|
||||
(persisted-tab-name tab)
|
||||
(persisted-tab-tracks tab)))
|
||||
(list (playlist-tab (uuid-string) "Default" '()))))
|
||||
(initial-context (restore-playlist-context store "anonymous" libraries))
|
||||
(tabs (playlist-context-tabs initial-context))
|
||||
(selected-index 0)
|
||||
(contexts (make-hash))
|
||||
(initial-context (playlist-context tabs selected-index)))
|
||||
(contexts (make-hash)))
|
||||
(hash-set! contexts "anonymous" initial-context)
|
||||
(define value
|
||||
(player libraries
|
||||
@@ -1324,12 +1393,17 @@
|
||||
value))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Return the complete browser-visible player state.
|
||||
; goal : Return browser-visible state, optionally reusing known playlist tracks.
|
||||
; pre : Value was created with make-player.
|
||||
; post : Cached DLNA playback information has been incorporated.
|
||||
; result : A JSON-compatible hash.
|
||||
; result : A JSON-compatible hash with playlistVersion. tracks is null when
|
||||
; playlist-version matches; otherwise it contains the complete list.
|
||||
; internals: tab-snapshot caches track JSON, duration and a unique version until
|
||||
; a playlist mutation invalidates it. Comparison and state assembly
|
||||
; share the player locks, so the version always matches the tracks.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (player-state->jsexpr value #:username [username "anonymous"])
|
||||
(define (player-state->jsexpr value #:username [username "anonymous"]
|
||||
#:playlist-version [playlist-version #f])
|
||||
(define normalized (normal-playlist-username username))
|
||||
(call-with-semaphore
|
||||
(player-command-lock value)
|
||||
@@ -1340,7 +1414,7 @@
|
||||
(define context (playlist-context-for! value normalized))
|
||||
(define tabs (playlist-context-tabs context))
|
||||
(define tab-index (playlist-context-current-index context))
|
||||
(define tracks (playlist-tab-tracks (list-ref tabs tab-index)))
|
||||
(define snapshot (tab-snapshot (list-ref tabs tab-index)))
|
||||
(with-state-lock
|
||||
value
|
||||
(λ ()
|
||||
@@ -1366,12 +1440,17 @@
|
||||
'tabs
|
||||
(for/list ((tab (in-list tabs))
|
||||
(index (in-naturals)))
|
||||
(tab->jsexpr tab index))
|
||||
(tab->jsexpr tab index (and (memq tab (playlist-context-saved context)) #t)))
|
||||
'savedPlaylists
|
||||
(map (λ (tab index) (tab->jsexpr tab index #t))
|
||||
(playlist-context-saved context)
|
||||
(range (length (playlist-context-saved context))))
|
||||
'currentTab tab-index
|
||||
'playlistVersion (hash-ref snapshot 'version)
|
||||
'tracks
|
||||
(for/list ((item (in-list tracks))
|
||||
(index (in-naturals)))
|
||||
(track->jsexpr item index))
|
||||
(if (equal? playlist-version (hash-ref snapshot 'version))
|
||||
'null
|
||||
(hash-ref snapshot 'tracks))
|
||||
'renderers (map renderer->jsexpr
|
||||
(player-renderers value))
|
||||
'rendererId (or (playback-session-selected-id session) 'null)
|
||||
@@ -1401,14 +1480,16 @@
|
||||
; goal : Execute one browser player command.
|
||||
; pre : Command is a string and data is a JSON object hash.
|
||||
; post : The command has completed or a concrete exception is raised.
|
||||
; result : The updated JSON-compatible player state.
|
||||
; result : The updated JSON-compatible player state. With a matching
|
||||
; playlist-version, tracks is null as in player-state->jsexpr.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define playlist-context-commands
|
||||
'("item-play" "item-add" "track-remove" "track-move"
|
||||
"playlist-clear" "tab-add" "tab-select" "tab-rename"
|
||||
"tab-delete" "play"))
|
||||
"tab-delete" "playlist-save" "playlist-open" "playlist-play" "play"))
|
||||
|
||||
(define (player-command! value command data #:username [username "anonymous"])
|
||||
(define (player-command! value command data #:username [username "anonymous"]
|
||||
#:playlist-version [playlist-version #f])
|
||||
(define normalized (normal-playlist-username username))
|
||||
(call-with-semaphore
|
||||
(player-command-lock value)
|
||||
@@ -1426,7 +1507,8 @@
|
||||
(when (member command playlist-context-commands)
|
||||
(activate-playlist-user! value normalized session))
|
||||
(perform-command! value session command data))))
|
||||
(player-state->jsexpr value #:username normalized))
|
||||
(player-state->jsexpr value #:username normalized
|
||||
#:playlist-version playlist-version))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Discover UPnP renderers and logical Sonos groups asynchronously.
|
||||
@@ -1739,16 +1821,176 @@
|
||||
(λ ()
|
||||
(set-player-closed?! value #t)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Tests for module player.rkt
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(module+ test
|
||||
(require rackunit
|
||||
(require json
|
||||
rackunit
|
||||
racket/file)
|
||||
|
||||
(test-case "playlist versions cache 1000 tracks and follow user mutations"
|
||||
(let ((value (make-player '() #:playlist-keystore #f #:local-output? #f)))
|
||||
(dynamic-wind
|
||||
void
|
||||
(λ ()
|
||||
(let* ((session (playback-session-for! value "anonymous"))
|
||||
(tracks
|
||||
(build-list
|
||||
1000
|
||||
(λ (index)
|
||||
(track (build-path (find-system-path 'temp-dir)
|
||||
(format "playlist-test-~a.flac" index))
|
||||
(format "Track ~a" index) "Artist" "Album" 60 "audio/flac")))))
|
||||
(replace-tracks! value session tracks)
|
||||
(let* ((full (player-state->jsexpr value))
|
||||
(version (hash-ref full 'playlistVersion))
|
||||
(compact (player-state->jsexpr value #:playlist-version version))
|
||||
(repeated (player-state->jsexpr value)))
|
||||
(check-equal? (length (hash-ref full 'tracks)) 1000)
|
||||
(check-equal? (hash-ref (car (hash-ref full 'tabs)) 'duration) 60000)
|
||||
(check-eq? (hash-ref compact 'tracks) 'null)
|
||||
(check-eq? (hash-ref full 'tracks) (hash-ref repeated 'tracks))
|
||||
(check-true (< (bytes-length (jsexpr->bytes compact))
|
||||
(/ (bytes-length (jsexpr->bytes full)) 20)))
|
||||
(check-eq?
|
||||
(hash-ref (player-command! value "repeat" (hasheq 'mode "all")
|
||||
#:playlist-version version) 'tracks)
|
||||
'null)
|
||||
(check-eq?
|
||||
(hash-ref (player-command! value "tab-rename" (hasheq 'index 0 'name "Music")
|
||||
#:playlist-version version) 'tracks)
|
||||
'null)
|
||||
(let* ((moved (player-command! value "track-move" (hasheq 'from 0 'to 1)
|
||||
#:playlist-version version))
|
||||
(moved-version (hash-ref moved 'playlistVersion))
|
||||
(other-browser (player-state->jsexpr value #:playlist-version version)))
|
||||
(check-not-equal? moved-version version)
|
||||
(check-equal? (hash-ref (car (hash-ref moved 'tracks)) 'title) "Track 1")
|
||||
(check-equal? (hash-ref other-browser 'playlistVersion) moved-version)
|
||||
(check-equal? (hash-ref other-browser 'tracks) (hash-ref moved 'tracks))
|
||||
(let* ((removed (player-command! value "track-remove" (hasheq 'index 0)
|
||||
#:playlist-version moved-version))
|
||||
(removed-version (hash-ref removed 'playlistVersion))
|
||||
(new-tab (player-command! value "tab-add" (hasheq)
|
||||
#:playlist-version removed-version))
|
||||
(selected (player-command! value "tab-select" (hasheq 'index 0)
|
||||
#:playlist-version (hash-ref new-tab 'playlistVersion))))
|
||||
(check-equal? (length (hash-ref removed 'tracks)) 999)
|
||||
(check-not-equal? removed-version moved-version)
|
||||
(check-equal? (hash-ref new-tab 'tracks) '())
|
||||
(check-equal? (hash-ref selected 'playlistVersion) removed-version)
|
||||
(check-equal? (length (hash-ref selected 'tracks)) 999)
|
||||
(let ((other-user (player-state->jsexpr value #:username "another-user"
|
||||
#:playlist-version removed-version))
|
||||
(cleared (player-command! value "playlist-clear" (hasheq)
|
||||
#:playlist-version removed-version)))
|
||||
(check-equal? (hash-ref other-user 'tracks) '())
|
||||
(check-not-equal? (hash-ref other-user 'playlistVersion) removed-version)
|
||||
(check-equal? (hash-ref cleared 'tracks) '())
|
||||
(check-not-equal? (hash-ref cleared 'playlistVersion) removed-version)
|
||||
(append-tracks! value session (list (car tracks)))
|
||||
(let* ((added (player-state->jsexpr value
|
||||
#:playlist-version (hash-ref cleared 'playlistVersion)))
|
||||
(added-version (hash-ref added 'playlistVersion)))
|
||||
(check-equal? (length (hash-ref added 'tracks)) 1)
|
||||
(check-equal? (hash-ref (car (hash-ref added 'tabs)) 'duration) 60)
|
||||
(append-tracks! value session (list (car tracks)))
|
||||
(check-eq? (hash-ref (player-state->jsexpr value #:playlist-version added-version)
|
||||
'tracks)
|
||||
'null))))))))
|
||||
(λ () (player-close! value)))))
|
||||
|
||||
(check-equal? (error->jsexpr #f) 'null)
|
||||
(check-equal?
|
||||
(error->jsexpr 'dlna-renderer-unreachable)
|
||||
"dlna-renderer-unreachable")
|
||||
(check-equal? (error->jsexpr "technical error") "technical error")
|
||||
|
||||
(test-case "saved playlists reopen as shared tabs and survive closing and restart"
|
||||
(let ((root (make-temporary-file "saved-playlists-~a" 'directory)))
|
||||
(dynamic-wind
|
||||
(λ ()
|
||||
(call-with-output-file (build-path root "one.flac") void)
|
||||
(call-with-output-file (build-path root "two.flac") void))
|
||||
(λ ()
|
||||
(let* ((libraries (make-music-libraries (list root)))
|
||||
(store-file (build-path root "playlists.keystore"))
|
||||
(agent-id (make-string 64 #\b))
|
||||
(value (make-player libraries #:playlist-keystore store-file
|
||||
#:local-output? #f #:allowed-agent-ids (list agent-id)))
|
||||
(session (playback-session-for! value "anonymous"))
|
||||
(first (track (build-path root "one.flac") "One" "Artist" "Album" 60 "audio/flac"))
|
||||
(second (track (build-path root "two.flac") "Two" "Artist" "Album" 120 "audio/flac")))
|
||||
(dynamic-wind
|
||||
void
|
||||
(λ ()
|
||||
(replace-tracks! value session (list first second))
|
||||
(let* ((initial (player-state->jsexpr value))
|
||||
(id (hash-ref (car (hash-ref initial 'tabs)) 'id))
|
||||
(version (hash-ref initial 'playlistVersion))
|
||||
(saved (player-command! value "playlist-save" (hasheq 'id id 'name "Favorites")
|
||||
#:playlist-version version)))
|
||||
(check-equal? (hash-ref initial 'savedPlaylists) '())
|
||||
(check-eq? (hash-ref saved 'tracks) 'null)
|
||||
(check-true (hash-ref (car (hash-ref saved 'tabs)) 'saved))
|
||||
(check-equal? (hash-ref (car (hash-ref saved 'savedPlaylists)) 'id) id)
|
||||
(check-equal? (hash-ref (car (hash-ref saved 'savedPlaylists)) 'duration) 180)
|
||||
(check-equal?
|
||||
(length (hash-ref (player-command! value "playlist-save" (hasheq 'id id 'name "Favorites"))
|
||||
'savedPlaylists)) 1)
|
||||
(player-command! value "tab-add" (hasheq))
|
||||
(replace-tracks! value session (list first))
|
||||
(let ((opened (player-command! value "playlist-open" (hasheq 'id id))))
|
||||
(check-equal? (length (hash-ref opened 'tabs)) 2)
|
||||
(check-equal? (hash-ref opened 'currentTab) 0)
|
||||
(check-equal? (map track-title (playlist-tab-tracks (list-ref (player-tabs value) 1))) '("One"))
|
||||
(check-equal? (length (hash-ref (player-command! value "playlist-open" (hasheq 'id id)) 'tabs)) 2))
|
||||
(player-command! value "tab-rename" (hasheq 'index 0 'name "Renamed"))
|
||||
(let ((edited (player-command! value "track-remove" (hasheq 'index 0))))
|
||||
(check-equal? (hash-ref (car (hash-ref edited 'savedPlaylists)) 'name) "Renamed")
|
||||
(check-equal? (hash-ref (car (hash-ref edited 'savedPlaylists)) 'count) 1)
|
||||
(check-equal? (hash-ref (car (hash-ref edited 'savedPlaylists)) 'duration) 120))
|
||||
(player-command! value "tab-delete" (hasheq 'index 0))
|
||||
(let ((opened (player-command! value "playlist-open" (hasheq 'id id))))
|
||||
(check-equal? (length (hash-ref opened 'tabs)) 2)
|
||||
(check-equal? (hash-ref opened 'currentTab) 1)
|
||||
(check-equal? (map (λ (track) (hash-ref track 'title)) (hash-ref opened 'tracks)) '("Two")))
|
||||
(player-agent-register! value (hasheq 'appId agent-id 'name "Test agent"))
|
||||
(player-command! value "renderer" (hasheq 'id (agent-renderer-id agent-id)))
|
||||
(let ((playing (player-command! value "playlist-play" (hasheq 'id id))))
|
||||
(check-equal? (hash-ref playing 'currentIndex) 0)
|
||||
(check-equal? (length (hash-ref playing 'tabs)) 2)
|
||||
(check-equal?
|
||||
(hash-ref (hash-ref (player-agent-poll! value (hasheq 'appId agent-id)) 'command) 'action)
|
||||
"play"))
|
||||
(player-command! value "tab-delete" (hasheq 'index 1))
|
||||
(check-equal? (hash-ref (player-state->jsexpr value #:username "other") 'savedPlaylists) '())
|
||||
(check-exn exn:fail?
|
||||
(λ () (player-command! value "playlist-open" (hasheq 'id id) #:username "other")))
|
||||
(player-close! value)
|
||||
(let ((restored (make-player libraries #:playlist-keystore store-file #:local-output? #f)))
|
||||
(dynamic-wind
|
||||
void
|
||||
(λ ()
|
||||
(let ((state (player-state->jsexpr restored)))
|
||||
(check-equal? (length (hash-ref state 'tabs)) 1)
|
||||
(check-equal? (hash-ref (car (hash-ref state 'savedPlaylists)) 'id) id))
|
||||
(player-command! restored "playlist-open" (hasheq 'id id))
|
||||
(let ((renamed (player-command! restored "tab-rename" (hasheq 'index 1 'name "Restored"))))
|
||||
(check-equal? (hash-ref (car (hash-ref renamed 'savedPlaylists)) 'name) "Restored")
|
||||
(check-equal? (map (λ (track) (hash-ref track 'title)) (hash-ref renamed 'tracks)) '("Two")))
|
||||
(player-command! restored "tab-delete" (hasheq 'index 0))
|
||||
(let ((closed (player-command! restored "tab-delete" (hasheq 'index 0))))
|
||||
(check-equal? (length (hash-ref closed 'tabs)) 1)
|
||||
(check-false (hash-ref (car (hash-ref closed 'tabs)) 'saved))
|
||||
(check-equal? (hash-ref closed 'tracks) '())
|
||||
(check-equal? (hash-ref (car (hash-ref closed 'savedPlaylists)) 'id) id)))
|
||||
(λ () (player-close! restored))))))
|
||||
(λ () (player-close! value)))))
|
||||
(λ () (delete-directory/files root)))))
|
||||
|
||||
(define root
|
||||
(make-temporary-file "rkt-web-player-~a" 'directory))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user