|
229 | 229 | end function |
230 | 230 |
|
231 | 231 | 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 | + |
235 | 243 | if isValid(serverUrl) |
236 | | - serverUrl = LCase(serverUrl)'Saved server data is always lowercase |
| 244 | + serverUrl = LCase(serverUrl) ' canonical URL always lowercase for comparison |
237 | 245 | 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 | + |
240 | 250 | savedServers = { serverList: [] } |
| 251 | + saved = get_setting("saved_servers") |
241 | 252 | 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 |
251 | 256 | end if |
252 | 257 | end if |
253 | 258 |
|
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 |
259 | 276 | set_setting("saved_servers", FormatJson(savedServers)) |
| 277 | + return |
260 | 278 | 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)) |
262 | 292 | end sub |
263 | 293 |
|
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). |
265 | 300 | 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)) |
279 | 318 | end sub |
280 | 319 |
|
281 | 320 | ' Roku Performance monitoring |
|
377 | 416 | else if nodeName = "delete_saved" |
378 | 417 | serverPicker = screen.findNode("serverPicker") |
379 | 418 | 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) |
383 | 427 | serverPicker.content.removeChild(itemToDelete) |
384 | 428 | sidepanel.visible = false |
385 | 429 | serverPicker.setFocus(true) |
|
0 commit comments