Skip to content

Commit aae5b4f

Browse files
committed
Update code docs
1 parent b9b6dcb commit aae5b4f

7 files changed

Lines changed: 224 additions & 60 deletions

docs/components_config_JRServer.bs.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@
2727

2828
m.poster.uri = serverItem.iconUrl
2929
m.name.text = serverItem.name
30-
m.baseUrl.text = serverItem.baseUrl
30+
' Prefer originalUrl (user-entered or scheme-stripped) over the canonical baseUrl.
31+
' Falls back to baseUrl for SSDP-discovered servers with no saved entry and for
32+
' old saved_servers entries that predate this field.
33+
displayUrl = serverItem.originalUrl
34+
if not isValidAndNotEmpty(displayUrl)
35+
displayUrl = serverItem.baseUrl
36+
end if
37+
m.baseUrl.text = displayUrl
3138
end sub
3239

3340
sub onFocusPercentChange()

docs/components_config_ServerDiscoveryTask.bs.html

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,81 @@
4141

4242
end while
4343

44+
FetchServerIds()
4445
m.top.content = m.servers
4546
m.log.debug("Jellyfin servers found", m.servers[0], m.servers[1], m.servers[2])
4647
end sub
4748

49+
' Fetch server IDs and names from /system/info/public for all discovered servers.
50+
' All requests are fired in parallel on a shared port and collected within a 5s budget.
51+
' Also strips the URL scheme to produce originalUrl (e.g. "192.168.1.100:8096") so:
52+
' - users see clean URLs in the server picker (no "http://" prefix)
53+
' - inferServerUrl() can re-probe the correct protocol on each connection,
54+
' automatically picking up HTTPS if the server has been upgraded since last save
55+
sub FetchServerIds()
56+
if m.servers.Count() = 0 then return
57+
58+
port = CreateObject("roMessagePort")
59+
identityToIndex = {}
60+
transfers = [] ' hold refs — roUrlTransfer is GC'd if not kept in scope
61+
62+
for i = 0 to m.servers.Count() - 1
63+
serverUrl = m.servers[i].baseUrl
64+
if not isValidAndNotEmpty(serverUrl) then continue for
65+
66+
http = CreateObject("roUrlTransfer")
67+
http.SetUrl(serverUrl + "/system/info/public")
68+
http.SetMessagePort(port)
69+
if serverUrl.Left(8) = "https://"
70+
http.SetCertificatesFile("common:/certs/ca-bundle.crt")
71+
end if
72+
http.AsyncGetToString()
73+
identityToIndex[http.GetIdentity().ToStr()] = i
74+
transfers.push(http)
75+
end for
76+
77+
remaining = transfers.Count()
78+
ts = CreateObject("roTimespan")
79+
80+
while remaining > 0 and ts.TotalMilliseconds() < 5000
81+
resp = wait(500, port)
82+
if type(resp) = "roUrlEvent"
83+
idx = identityToIndex[resp.GetSourceIdentity().ToStr()]
84+
if isValid(idx) and resp.GetResponseCode() = 200
85+
info = ParseJson(resp.GetString())
86+
if isValid(info)
87+
if isValidAndNotEmpty(info.Id)
88+
m.servers[idx].id = info.Id
89+
end if
90+
if isValidAndNotEmpty(info.ServerName)
91+
m.servers[idx].name = info.ServerName
92+
end if
93+
end if
94+
end if
95+
remaining--
96+
end if
97+
end while
98+
99+
' Strip URL scheme from every discovered server to produce a clean originalUrl.
100+
' inferServerUrl() probes http/https candidates in parallel so there is no reconnect
101+
' penalty for servers that are HTTP-only.
102+
for i = 0 to m.servers.Count() - 1
103+
serverUrl = m.servers[i].baseUrl
104+
if not isValidAndNotEmpty(serverUrl) then continue for
105+
106+
schemeEnd = Instr(1, serverUrl, "://")
107+
if schemeEnd > 0
108+
originalUrl = Mid(serverUrl, schemeEnd + 3)
109+
else
110+
originalUrl = serverUrl
111+
end if
112+
if Right(originalUrl, 1) = "/" and Len(originalUrl) > 1
113+
originalUrl = Left(originalUrl, Len(originalUrl) - 1)
114+
end if
115+
m.servers[i].originalUrl = originalUrl
116+
end for
117+
end sub
118+
48119
sub AddServer(serverItem)
49120
if not isValid(m.serverUrlMap[serverItem.baseUrl])
50121
m.serverUrlMap[serverItem.baseUrl] = true

docs/components_config_SetServerScreen.bs.html

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,65 @@
3535

3636
items = CreateObject("roSGNode", "ContentNode")
3737
stopLoadingSpinner()
38+
39+
' Load saved servers upfront — used in both passes below
40+
savedServers = { serverList: [] }
41+
saved = get_setting("saved_servers")
42+
if isValid(saved)
43+
parsed = ParseJson(saved)
44+
if isValid(parsed) and isValid(parsed.serverList)
45+
savedServers = parsed
46+
end if
47+
end if
48+
49+
' Pass 1: Add all SSDP-discovered servers.
50+
' If a matching saved entry exists, inject its originalUrl so the user sees their own
51+
' URL (e.g. "192.168.1.100:8096" or an HTTPS address) instead of the raw SSDP URL.
52+
' Dedup uses id first (robust to URL changes), then falls back to canonical baseUrl
53+
' for backward compatibility with saved entries that predate this change.
3854
for each serverItem in m.servers
3955
serverItem.subtype = "ContentNode"
40-
'add new fields for every server property onto the ContentNode (rather than making a dedicated component just to hold data...)
56+
for each savedServer in savedServers.serverList
57+
isMatch = false
58+
if isValidAndNotEmpty(serverItem.id) and isValidAndNotEmpty(savedServer.id)
59+
isMatch = (serverItem.id = savedServer.id)
60+
else if LCase(serverItem.baseUrl) = savedServer.baseUrl
61+
isMatch = true
62+
end if
63+
if isMatch
64+
' Merge: keep SSDP name and connection URL, restore saved originalUrl
65+
savedOriginalUrl = savedServer.originalUrl
66+
if not isValidAndNotEmpty(savedOriginalUrl)
67+
savedOriginalUrl = savedServer.baseUrl ' backward compat: old entries have no originalUrl
68+
end if
69+
serverItem.originalUrl = savedOriginalUrl
70+
serverItem.isSaved = true ' mark as deletable — this server exists in saved_servers
71+
exit for
72+
end if
73+
end for
4174
items.update([serverItem], true)
4275
end for
4376

44-
'load any previously logged in to servers as well (if they aren't already discovered on the local network)
45-
saved = get_setting("saved_servers")
46-
if isValid(saved)
47-
savedServers = ParseJson(saved)
48-
for each savedServer in savedServers.serverList
49-
alreadyListed = false
50-
for each listed in m.servers
51-
if LCase(listed.baseUrl) = savedServer.baseUrl 'saved server data is always lowercase
77+
' Pass 2: Add saved servers not found by SSDP (e.g. remote servers)
78+
for each savedServer in savedServers.serverList
79+
alreadyListed = false
80+
for each listed in m.servers
81+
if isValidAndNotEmpty(listed.id) and isValidAndNotEmpty(savedServer.id)
82+
if listed.id = savedServer.id
5283
alreadyListed = true
5384
exit for
5485
end if
55-
end for
56-
if alreadyListed = false
57-
items.update([savedServer], true)
58-
m.servers.push(savedServer)
86+
else if LCase(listed.baseUrl) = savedServer.baseUrl
87+
alreadyListed = true
88+
exit for
5989
end if
6090
end for
61-
end if
91+
if not alreadyListed
92+
savedServer.isSaved = true ' mark as deletable — this server exists in saved_servers
93+
items.update([savedServer], true)
94+
m.servers.push(savedServer)
95+
end if
96+
end for
6297

6398
m.serverPicker.content = items
6499

@@ -130,7 +165,14 @@
130165
handled = true
131166

132167
if key = "OK" and m.serverPicker.hasFocus()
133-
m.top.serverUrl = m.serverPicker.content.getChild(m.serverPicker.itemFocused).baseUrl
168+
item = m.serverPicker.content.getChild(m.serverPicker.itemFocused)
169+
' Prefer originalUrl (user-entered or scheme-stripped SSDP URL) so inferServerUrl()
170+
' can re-discover the correct protocol rather than locking in the canonical form
171+
selectedUrl = item.originalUrl
172+
if not isValidAndNotEmpty(selectedUrl)
173+
selectedUrl = item.baseUrl
174+
end if
175+
m.top.serverUrl = selectedUrl
134176
m.buttons.setFocus(true)
135177
'if the user pressed the down key and we are already at the last child of server picker, then change focus to the url textbox
136178
else if key = "down" and m.serverPicker.hasFocus() and m.serverPicker.content.getChildCount() > 0 and m.serverPicker.itemFocused = m.serverPicker.content.getChildCount() - 1
@@ -161,10 +203,10 @@
161203
m.buttons.setFocus(true)
162204
else if key = "options"
163205
if m.serverPicker.itemFocused >= 0 and m.serverPicker.itemFocused < m.serverPicker.content.getChildCount()
164-
serverName = m.serverPicker.content.getChild(m.serverPicker.itemFocused).name
165-
if m.servers.Count() > 0 and Instr(1, serverName, "Saved") > 0
206+
item = m.serverPicker.content.getChild(m.serverPicker.itemFocused)
207+
if m.servers.Count() > 0 and item.isSaved = true
166208
'Can only delete previously saved servers, not locally discovered ones
167-
'So if we are on a "Saved" item, let the options dialog be shown (handled elsewhere)
209+
'So if we are on a saved item, let the options dialog be shown (handled elsewhere)
168210
handled = false
169211
end if
170212
end if

docs/data/search.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-ServerDiscoveryTask.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-ShowScenes.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/source_ShowScenes.bs.html

Lines changed: 82 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -229,53 +229,92 @@
229229
end function
230230

231231
sub SaveServerList()
232-
'Save off this server to our list of saved servers for easier navigation between servers
233-
serverUrl = m.global.server.serverUrl
234-
saved = get_setting("saved_servers")
232+
' Save this server to the list of previously-used servers shown in the server picker.
233+
' baseUrl: canonical URL (lowercase) — used for deduplication against SSDP-discovered servers.
234+
' originalUrl: user-entered URL from registry — shown in the picker and pre-filled on re-selection,
235+
' so inferServerUrl() can re-discover the correct protocol on each connection.
236+
' id: Jellyfin server ID — primary deduplication key, robust to URL changes (e.g. HTTP→HTTPS).
237+
globalServer = m.global.server
238+
serverUrl = globalServer.serverUrl
239+
serverId = globalServer.id
240+
serverName = globalServer.name
241+
originalUrl = get_setting("server") ' set correctly by server.UpdateURL() before this is called
242+
235243
if isValid(serverUrl)
236-
serverUrl = LCase(serverUrl)'Saved server data is always lowercase
244+
serverUrl = LCase(serverUrl) ' canonical URL always lowercase for comparison
237245
end if
238-
entryCount = 0
239-
addNewEntry = true
246+
if not isValidAndNotEmpty(originalUrl)
247+
originalUrl = serverUrl ' fallback: use canonical if original is somehow missing
248+
end if
249+
240250
savedServers = { serverList: [] }
251+
saved = get_setting("saved_servers")
241252
if isValid(saved)
242-
savedServers = ParseJson(saved)
243-
entryCount = savedServers.serverList.Count()
244-
if isValid(savedServers.serverList) and entryCount > 0
245-
for each item in savedServers.serverList
246-
if item.baseUrl = serverUrl
247-
addNewEntry = false
248-
exit for
249-
end if
250-
end for
253+
parsed = ParseJson(saved)
254+
if isValid(parsed) and isValid(parsed.serverList)
255+
savedServers = parsed
251256
end if
252257
end if
253258

254-
if addNewEntry
255-
if entryCount = 0
256-
set_setting("saved_servers", FormatJson({ serverList: [{ name: m.serverSelection, baseUrl: serverUrl, iconUrl: "pkg:/images/branding/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 }] }))
257-
else
258-
savedServers.serverList.Push({ name: m.serverSelection, baseUrl: serverUrl, iconUrl: "pkg:/images/branding/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 })
259+
' Check for an existing entry (ID-based first; URL fallback for old entries without id)
260+
for i = 0 to savedServers.serverList.Count() - 1
261+
item = savedServers.serverList[i]
262+
isMatch = false
263+
if isValidAndNotEmpty(serverId) and isValidAndNotEmpty(item.id)
264+
isMatch = (item.id = serverId)
265+
else if LCase(item.baseUrl) = serverUrl
266+
isMatch = true
267+
end if
268+
269+
if isMatch
270+
' Update in-place: refresh mutable server identity fields (name, id, baseUrl, originalUrl).
271+
' iconUrl/iconWidth/iconHeight are static app branding defaults and are not changed.
272+
savedServers.serverList[i].name = serverName
273+
savedServers.serverList[i].id = serverId
274+
savedServers.serverList[i].baseUrl = serverUrl ' keep canonical URL current (e.g. HTTP→HTTPS)
275+
savedServers.serverList[i].originalUrl = originalUrl
259276
set_setting("saved_servers", FormatJson(savedServers))
277+
return
260278
end if
261-
end if
279+
end for
280+
281+
' No existing entry found — append a new one
282+
savedServers.serverList.Push({
283+
name: serverName,
284+
id: serverId,
285+
baseUrl: serverUrl,
286+
originalUrl: originalUrl,
287+
iconUrl: "pkg:/images/branding/logo-icon120.jpg",
288+
iconWidth: 120,
289+
iconHeight: 120
290+
})
291+
set_setting("saved_servers", FormatJson(savedServers))
262292
end sub
263293

264-
sub DeleteFromServerList(urlToDelete)
294+
sub DeleteFromServerList(idOrUrl as string)
295+
' idOrUrl should be the server's id when available (passed from itemToDelete.id).
296+
' Falls back to a canonical baseUrl for legacy entries that predate the id field.
297+
' ID match is tried first so deletion is correct even when the saved entry's baseUrl
298+
' differs from the picker item's baseUrl (e.g. a saved HTTPS entry matched via SSDP
299+
' on HTTP — the picker item carries the SSDP baseUrl, not the saved one).
265300
saved = get_setting("saved_servers")
266-
if isValid(urlToDelete)
267-
urlToDelete = LCase(urlToDelete)
268-
end if
269-
if isValid(saved)
270-
savedServers = ParseJson(saved)
271-
newServers = { serverList: [] }
272-
for each item in savedServers.serverList
273-
if item.baseUrl <> urlToDelete
274-
newServers.serverList.Push(item)
275-
end if
276-
end for
277-
set_setting("saved_servers", FormatJson(newServers))
278-
end if
301+
if not isValid(saved) then return
302+
303+
savedServers = ParseJson(saved)
304+
newServers = { serverList: [] }
305+
normalizedInput = LCase(idOrUrl) ' for URL fallback comparison (baseUrls are always lowercase)
306+
for each item in savedServers.serverList
307+
keepEntry = true
308+
if isValidAndNotEmpty(item.id) and item.id = idOrUrl
309+
keepEntry = false ' ID match — remove this entry
310+
else if item.baseUrl = normalizedInput
311+
keepEntry = false ' URL fallback — remove this entry
312+
end if
313+
if keepEntry
314+
newServers.serverList.Push(item)
315+
end if
316+
end for
317+
set_setting("saved_servers", FormatJson(newServers))
279318
end sub
280319

281320
' Roku Performance monitoring
@@ -377,9 +416,14 @@
377416
else if nodeName = "delete_saved"
378417
serverPicker = screen.findNode("serverPicker")
379418
itemToDelete = serverPicker.content.getChild(serverPicker.itemFocused)
380-
urlToDelete = itemToDelete.baseUrl
381-
if isValid(urlToDelete)
382-
DeleteFromServerList(urlToDelete)
419+
' Prefer id for deletion — robust when the picker item's baseUrl differs from
420+
' the saved entry's baseUrl (e.g. SSDP HTTP item merged from an HTTPS saved entry)
421+
idOrUrl = itemToDelete.id
422+
if not isValidAndNotEmpty(idOrUrl)
423+
idOrUrl = itemToDelete.baseUrl
424+
end if
425+
if isValidAndNotEmpty(idOrUrl)
426+
DeleteFromServerList(idOrUrl)
383427
serverPicker.content.removeChild(itemToDelete)
384428
sidepanel.visible = false
385429
serverPicker.setFocus(true)

0 commit comments

Comments
 (0)