2023-11-11 13:41:20 +00:00
|
|
|
<!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: source/utils/misc.bs</title><!--[if lt IE 9]>
|
|
|
|
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
2023-12-05 16:56:00 +00:00
|
|
|
<![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(baseURL=(baseURL="https://jellyfin.github.io/jellyfin-roku/").replace(/https?:\/\//i,"")).substr(baseURL.indexOf("/"))</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d=
|
2023-10-31 21:36:19 +00:00
|
|
|
|
|
|
|
function isNodeEvent(msg, field as string) as boolean
|
2023-10-06 03:18:36 +00:00
|
|
|
return type(msg) = "roSGNodeEvent" and msg.getField() = field
|
|
|
|
end function
|
|
|
|
|
|
|
|
|
|
|
|
function getMsgPicker(msg, subnode = "" as string) as object
|
|
|
|
node = msg.getRoSGNode()
|
|
|
|
' Subnode allows for handling alias messages
|
|
|
|
if subnode <> ""
|
|
|
|
node = node.findNode(subnode)
|
|
|
|
end if
|
|
|
|
coords = node.rowItemSelected
|
|
|
|
target = node.content.getChild(coords[0]).getChild(coords[1])
|
|
|
|
return target
|
|
|
|
end function
|
|
|
|
|
|
|
|
function getButton(msg, subnode = "buttons" as string) as object
|
|
|
|
buttons = msg.getRoSGNode().findNode(subnode)
|
|
|
|
if buttons = invalid then return invalid
|
|
|
|
active_button = buttons.focusedChild
|
|
|
|
return active_button
|
|
|
|
end function
|
|
|
|
|
|
|
|
function leftPad(base as string, fill as string, length as integer) as string
|
|
|
|
while len(base) < length
|
|
|
|
base = fill + base
|
|
|
|
end while
|
|
|
|
return base
|
|
|
|
end function
|
|
|
|
|
|
|
|
function ticksToHuman(ticks as longinteger) as string
|
|
|
|
totalSeconds = int(ticks / 10000000)
|
|
|
|
hours = stri(int(totalSeconds / 3600)).trim()
|
|
|
|
minutes = stri(int((totalSeconds - (val(hours) * 3600)) / 60)).trim()
|
|
|
|
seconds = stri(totalSeconds - (val(hours) * 3600) - (val(minutes) * 60)).trim()
|
|
|
|
if val(hours) > 0 and val(minutes) < 10 then minutes = "0" + minutes
|
|
|
|
if val(seconds) < 10 then seconds = "0" + seconds
|
|
|
|
r = ""
|
|
|
|
if val(hours) > 0 then r = hours + ":"
|
|
|
|
r = r + minutes + ":" + seconds
|
|
|
|
return r
|
|
|
|
end function
|
|
|
|
|
2023-11-12 18:13:07 +00:00
|
|
|
function secondsToHuman(totalSeconds as integer, addLeadingMinuteZero as boolean) as string
|
|
|
|
humanTime = ""
|
2023-10-06 03:18:36 +00:00
|
|
|
hours = stri(int(totalSeconds / 3600)).trim()
|
|
|
|
minutes = stri(int((totalSeconds - (val(hours) * 3600)) / 60)).trim()
|
|
|
|
seconds = stri(totalSeconds - (val(hours) * 3600) - (val(minutes) * 60)).trim()
|
2023-11-12 18:13:07 +00:00
|
|
|
|
|
|
|
if val(hours) > 0 or addLeadingMinuteZero
|
|
|
|
if val(minutes) < 10
|
|
|
|
minutes = "0" + minutes
|
|
|
|
end if
|
|
|
|
end if
|
|
|
|
|
|
|
|
if val(seconds) < 10
|
|
|
|
seconds = "0" + seconds
|
|
|
|
end if
|
|
|
|
|
|
|
|
if val(hours) > 0
|
|
|
|
hours = hours + ":"
|
|
|
|
else
|
|
|
|
hours = ""
|
|
|
|
end if
|
|
|
|
|
|
|
|
humanTime = hours + minutes + ":" + seconds
|
|
|
|
|
|
|
|
return humanTime
|
2023-10-06 03:18:36 +00:00
|
|
|
end function
|
|
|
|
|
|
|
|
' Format time as 12 or 24 hour format based on system clock setting
|
|
|
|
function formatTime(time) as string
|
|
|
|
hours = time.getHours()
|
|
|
|
minHourDigits = 1
|
|
|
|
if m.global.device.clockFormat = "12h"
|
|
|
|
meridian = "AM"
|
|
|
|
if hours = 0
|
|
|
|
hours = 12
|
|
|
|
meridian = "AM"
|
|
|
|
else if hours = 12
|
|
|
|
hours = 12
|
|
|
|
meridian = "PM"
|
|
|
|
else if hours > 12
|
|
|
|
hours = hours - 12
|
|
|
|
meridian = "PM"
|
|
|
|
end if
|
|
|
|
else
|
|
|
|
' For 24hr Clock, no meridian and pad hours to 2 digits
|
|
|
|
minHourDigits = 2
|
|
|
|
meridian = ""
|
|
|
|
end if
|
|
|
|
|
|
|
|
return Substitute("{0}:{1} {2}", leftPad(stri(hours).trim(), "0", minHourDigits), leftPad(stri(time.getMinutes()).trim(), "0", 2), meridian)
|
|
|
|
|
|
|
|
end function
|
|
|
|
|
|
|
|
function div_ceiling(a as integer, b as integer) as integer
|
|
|
|
if a < b then return 1
|
|
|
|
if int(a / b) = a / b
|
|
|
|
return a / b
|
|
|
|
end if
|
|
|
|
return a / b + 1
|
|
|
|
end function
|
|
|
|
|
|
|
|
'Returns the item selected or -1 on backpress or other unhandled closure of dialog.
|
|
|
|
function get_dialog_result(dialog, port)
|
|
|
|
while dialog <> invalid
|
|
|
|
msg = wait(0, port)
|
|
|
|
if isNodeEvent(msg, "backPressed")
|
|
|
|
return -1
|
|
|
|
else if isNodeEvent(msg, "itemSelected")
|
|
|
|
return dialog.findNode("optionList").itemSelected
|
|
|
|
end if
|
|
|
|
end while
|
|
|
|
'Dialog has closed outside of this loop, return -1 for failure
|
|
|
|
return -1
|
|
|
|
end function
|
|
|
|
|
|
|
|
function lastFocusedChild(obj as object) as object
|
|
|
|
if isValid(obj)
|
|
|
|
if isValid(obj.focusedChild) and isValid(obj.focusedChild.focusedChild) and LCase(obj.focusedChild.focusedChild.subType()) = "tvepisodes"
|
|
|
|
if isValid(obj.focusedChild.focusedChild.lastFocus)
|
|
|
|
return obj.focusedChild.focusedChild.lastFocus
|
|
|
|
end if
|
|
|
|
end if
|
|
|
|
|
|
|
|
child = obj
|
|
|
|
for i = 0 to obj.getChildCount()
|
|
|
|
if isValid(obj.focusedChild)
|
|
|
|
child = child.focusedChild
|
|
|
|
end if
|
|
|
|
end for
|
|
|
|
return child
|
|
|
|
else
|
|
|
|
return invalid
|
|
|
|
end if
|
|
|
|
end function
|
|
|
|
|
|
|
|
function show_dialog(message as string, options = [], defaultSelection = 0) as integer
|
|
|
|
lastFocus = lastFocusedChild(m.scene)
|
|
|
|
|
|
|
|
dialog = createObject("roSGNode", "JFMessageDialog")
|
|
|
|
if options.count() then dialog.options = options
|
|
|
|
if message.len() > 0
|
|
|
|
reg = CreateObject("roFontRegistry")
|
|
|
|
font = reg.GetDefaultFont()
|
|
|
|
dialog.fontHeight = font.GetOneLineHeight()
|
|
|
|
dialog.fontWidth = font.GetOneLineWidth(message, 999999999)
|
|
|
|
dialog.message = message
|
|
|
|
end if
|
|
|
|
|
|
|
|
if defaultSelection > 0
|
|
|
|
dialog.findNode("optionList").jumpToItem = defaultSelection
|
|
|
|
end if
|
|
|
|
|
|
|
|
dialog.visible = true
|
|
|
|
m.scene.appendChild(dialog)
|
|
|
|
dialog.setFocus(true)
|
|
|
|
|
|
|
|
port = CreateObject("roMessagePort")
|
|
|
|
dialog.observeField("backPressed", port)
|
|
|
|
dialog.findNode("optionList").observeField("itemSelected", port)
|
|
|
|
|
|
|
|
result = get_dialog_result(dialog, port)
|
|
|
|
|
|
|
|
m.scene.removeChildIndex(m.scene.getChildCount() - 1)
|
|
|
|
lastFocus.setFocus(true)
|
|
|
|
|
|
|
|
return result
|
|
|
|
end function
|
|
|
|
|
|
|
|
function message_dialog(message = "" as string)
|
|
|
|
return show_dialog(message, ["OK"])
|
|
|
|
end function
|
|
|
|
|
|
|
|
function option_dialog(options, message = "", defaultSelection = 0) as integer
|
|
|
|
return show_dialog(message, options, defaultSelection)
|
|
|
|
end function
|
|
|
|
|
2023-10-31 21:36:19 +00:00
|
|
|
' take an incomplete url string and use it to make educated guesses about
|
|
|
|
' the complete url. then tests these guesses to see if it can find a jf server
|
|
|
|
' returns the url of the server it found, or an empty string
|
|
|
|
function inferServerUrl(url as string) as string
|
|
|
|
' if this server is already stored, just use the value directly
|
|
|
|
' the server had to get resolved in the first place to get into the registry
|
|
|
|
saved = get_setting("saved_servers")
|
|
|
|
if isValid(saved)
|
|
|
|
savedServers = ParseJson(saved)
|
|
|
|
if isValid(savedServers.lookup(url)) then return url
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
2023-10-31 21:36:19 +00:00
|
|
|
|
|
|
|
port = CreateObject("roMessagePort")
|
|
|
|
hosts = CreateObject("roAssociativeArray")
|
|
|
|
reqs = []
|
|
|
|
candidates = urlCandidates(url)
|
|
|
|
for each endpoint in candidates
|
|
|
|
req = CreateObject("roUrlTransfer")
|
|
|
|
reqs.push(req) ' keep in scope outside of loop, else -10001
|
|
|
|
req.seturl(endpoint + "/system/info/public")
|
|
|
|
req.setMessagePort(port)
|
|
|
|
hosts.addreplace(req.getidentity().ToStr(), endpoint)
|
|
|
|
if endpoint.Left(8) = "https://"
|
|
|
|
req.setCertificatesFile("common:/certs/ca-bundle.crt")
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
2023-10-31 21:36:19 +00:00
|
|
|
req.AsyncGetToString()
|
|
|
|
end for
|
|
|
|
handled = 0
|
|
|
|
timeout = CreateObject("roTimespan")
|
|
|
|
if hosts.count() > 0
|
|
|
|
while timeout.totalseconds() < 15
|
|
|
|
resp = wait(0, port)
|
|
|
|
if type(resp) = "roUrlEvent"
|
|
|
|
' TODO
|
|
|
|
' if response code is a 300 redirect then we should return the redirect url
|
|
|
|
' Make sure this happens or make it happen
|
|
|
|
if resp.GetResponseCode() = 200 and isJellyfinServer(resp.GetString())
|
|
|
|
selectedUrl = hosts.lookup(resp.GetSourceIdentity().ToStr())
|
|
|
|
print "Successfully inferred server URL: " selectedUrl
|
|
|
|
return selectedUrl
|
|
|
|
end if
|
|
|
|
end if
|
|
|
|
handled += 1
|
|
|
|
if handled = reqs.count()
|
|
|
|
print "inferServerUrl in utils/misc.brs failed to find a server from the string " url " but did not timeout."
|
|
|
|
return ""
|
|
|
|
end if
|
|
|
|
end while
|
|
|
|
print "inferServerUrl in utils/misc.brs failed to find a server from the string " url " because it timed out."
|
|
|
|
end if
|
|
|
|
return ""
|
|
|
|
end function
|
|
|
|
|
|
|
|
' this is the "educated guess" logic for inferServerUrl that generates a list of complete url's as candidates
|
|
|
|
' for the tests in inferServerUrl. takes an incomplete url as an arg and returns a list of extrapolated
|
|
|
|
' full urls.
|
|
|
|
function urlCandidates(input as string)
|
|
|
|
if input.endswith("/") then input = input.Left(len(input) - 1)
|
|
|
|
url = parseUrl(input)
|
|
|
|
if url[1] = invalid
|
|
|
|
' a proto wasn't declared
|
|
|
|
url = parseUrl("none://" + input)
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
2023-10-31 21:36:19 +00:00
|
|
|
' if the proto is still invalid then the string is not valid
|
|
|
|
if url[1] = invalid then return []
|
|
|
|
proto = url[1]
|
|
|
|
host = url[2]
|
|
|
|
port = url[3]
|
|
|
|
path = url[4]
|
|
|
|
protoCandidates = []
|
|
|
|
supportedProtos = ["http:", "https:"] ' appending colons because the regex does
|
|
|
|
if proto = "none:" ' the user did not declare a protocol
|
|
|
|
' try every supported proto
|
|
|
|
for each supportedProto in supportedProtos
|
|
|
|
protoCandidates.push(supportedProto + "//" + host)
|
|
|
|
end for
|
|
|
|
else
|
|
|
|
protoCandidates.push(proto + "//" + host) ' but still allow arbitrary protocols if they are declared
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
2023-10-31 21:36:19 +00:00
|
|
|
finalCandidates = []
|
|
|
|
if isValid(port) and port <> "" ' if the port is defined just use that
|
|
|
|
for each candidate in protoCandidates
|
|
|
|
finalCandidates.push(candidate + port + path)
|
|
|
|
end for
|
|
|
|
else ' the port wasnt declared so use default jellyfin and proto ports
|
|
|
|
for each candidate in protoCandidates:
|
|
|
|
' proto default
|
|
|
|
finalCandidates.push(candidate + path)
|
|
|
|
' jellyfin defaults
|
|
|
|
if candidate.startswith("https")
|
|
|
|
finalCandidates.push(candidate + ":8920" + path)
|
|
|
|
else if candidate.startswith("http")
|
|
|
|
finalCandidates.push(candidate + ":8096" + path)
|
|
|
|
end if
|
|
|
|
end for
|
|
|
|
end if
|
|
|
|
return finalCandidates
|
2023-10-06 03:18:36 +00:00
|
|
|
end function
|
|
|
|
|
|
|
|
sub setFieldTextValue(field, value)
|
|
|
|
node = m.top.findNode(field)
|
|
|
|
if node = invalid or value = invalid then return
|
|
|
|
|
|
|
|
' Handle non strings... Which _shouldn't_ happen, but hey
|
|
|
|
if type(value) = "roInt" or type(value) = "Integer"
|
|
|
|
value = str(value).trim()
|
|
|
|
else if type(value) = "roFloat" or type(value) = "Float"
|
|
|
|
value = str(value).trim()
|
|
|
|
else if type(value) <> "roString" and type(value) <> "String"
|
|
|
|
value = ""
|
|
|
|
end if
|
|
|
|
|
|
|
|
node.text = value
|
|
|
|
end sub
|
|
|
|
|
|
|
|
' Returns whether or not passed value is valid
|
|
|
|
function isValid(input as dynamic) as boolean
|
|
|
|
return input <> invalid
|
|
|
|
end function
|
|
|
|
|
|
|
|
' Returns whether or not passed value is valid and not empty
|
|
|
|
' Accepts a string, or any countable type (arrays and lists)
|
|
|
|
function isValidAndNotEmpty(input as dynamic) as boolean
|
|
|
|
if not isValid(input) then return false
|
|
|
|
' Use roAssociativeArray instead of list so we get access to the doesExist() method
|
|
|
|
countableTypes = { "array": 1, "list": 1, "roarray": 1, "roassociativearray": 1, "rolist": 1 }
|
|
|
|
inputType = LCase(type(input))
|
|
|
|
if inputType = "string" or inputType = "rostring"
|
|
|
|
trimmedInput = input.trim()
|
|
|
|
return trimmedInput <> ""
|
2023-11-15 00:22:33 +00:00
|
|
|
else if inputType = "rosgnode"
|
|
|
|
inputId = input.id
|
|
|
|
return inputId <> invalid
|
2023-10-06 03:18:36 +00:00
|
|
|
else if countableTypes.doesExist(inputType)
|
|
|
|
return input.count() > 0
|
|
|
|
else
|
|
|
|
print "Called isValidAndNotEmpty() with invalid type: ", inputType
|
|
|
|
return false
|
|
|
|
end if
|
|
|
|
end function
|
|
|
|
|
2023-10-31 21:36:19 +00:00
|
|
|
' Returns an array from a url = [ url, proto, host, port, subdir+params ]
|
2023-10-06 03:18:36 +00:00
|
|
|
' If port or subdir are not found, an empty string will be added to the array
|
|
|
|
' Proto must be declared or array will be empty
|
|
|
|
function parseUrl(url as string) as object
|
|
|
|
rgx = CreateObject("roRegex", "^(.*:)//([A-Za-z0-9\-\.]+)(:[0-9]+)?(.*)$", "")
|
|
|
|
return rgx.Match(url)
|
|
|
|
end function
|
|
|
|
|
|
|
|
' Returns true if the string is a loopback, such as 'localhost' or '127.0.0.1'
|
|
|
|
function isLocalhost(url as string) as boolean
|
|
|
|
' https://stackoverflow.com/questions/8426171/what-regex-will-match-all-loopback-addresses
|
|
|
|
rgx = CreateObject("roRegex", "^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$", "i")
|
|
|
|
return rgx.isMatch(url)
|
|
|
|
end function
|
|
|
|
|
|
|
|
' Rounds number to nearest integer
|
|
|
|
function roundNumber(f as float) as integer
|
|
|
|
' BrightScript only has a "floor" round
|
|
|
|
' This compares floor to floor + 1 to find which is closer
|
|
|
|
m = int(f)
|
|
|
|
n = m + 1
|
|
|
|
x = abs(f - m)
|
|
|
|
y = abs(f - n)
|
|
|
|
if y > x
|
|
|
|
return m
|
|
|
|
else
|
|
|
|
return n
|
|
|
|
end if
|
|
|
|
end function
|
|
|
|
|
|
|
|
' Converts ticks to minutes
|
|
|
|
function getMinutes(ticks) as integer
|
|
|
|
' A tick is .1ms, so 1/10,000,000 for ticks to seconds,
|
|
|
|
' then 1/60 for seconds to minutes... 1/600,000,000
|
|
|
|
return roundNumber(ticks / 600000000.0)
|
|
|
|
end function
|
|
|
|
|
|
|
|
'
|
|
|
|
' Returns whether or not a version number (e.g. 10.7.7) is greater or equal
|
|
|
|
' to some minimum version allowed (e.g. 10.8.0)
|
|
|
|
function versionChecker(versionToCheck as string, minVersionAccepted as string)
|
|
|
|
leftHand = CreateObject("roLongInteger")
|
|
|
|
rightHand = CreateObject("roLongInteger")
|
|
|
|
|
|
|
|
regEx = CreateObject("roRegex", "\.", "")
|
|
|
|
version = regEx.Split(versionToCheck)
|
|
|
|
if version.Count() < 3
|
|
|
|
for i = version.Count() to 3 step 1
|
|
|
|
version.AddTail("0")
|
|
|
|
end for
|
|
|
|
end if
|
|
|
|
|
|
|
|
minVersion = regEx.Split(minVersionAccepted)
|
|
|
|
if minVersion.Count() < 3
|
|
|
|
for i = minVersion.Count() to 3 step 1
|
|
|
|
minVersion.AddTail("0")
|
|
|
|
end for
|
|
|
|
end if
|
|
|
|
|
|
|
|
leftHand = (version[0].ToInt() * 10000) + (version[1].ToInt() * 100) + (version[2].ToInt() * 10)
|
|
|
|
rightHand = (minVersion[0].ToInt() * 10000) + (minVersion[1].ToInt() * 100) + (minVersion[2].ToInt() * 10)
|
|
|
|
|
|
|
|
return leftHand >= rightHand
|
|
|
|
end function
|
|
|
|
|
|
|
|
function findNodeBySubtype(node, subtype)
|
|
|
|
foundNodes = []
|
|
|
|
|
|
|
|
for each child in node.getChildren(-1, 0)
|
|
|
|
if lcase(child.subtype()) = "group"
|
|
|
|
return findNodeBySubtype(child, subtype)
|
|
|
|
end if
|
|
|
|
|
|
|
|
if lcase(child.subtype()) = lcase(subtype)
|
|
|
|
foundNodes.push({
|
|
|
|
node: child,
|
|
|
|
parent: node
|
|
|
|
})
|
|
|
|
end if
|
|
|
|
end for
|
|
|
|
|
|
|
|
return foundNodes
|
|
|
|
end function
|
|
|
|
|
|
|
|
function AssocArrayEqual(Array1 as object, Array2 as object) as boolean
|
|
|
|
if not isValid(Array1) or not isValid(Array2)
|
|
|
|
return false
|
|
|
|
end if
|
|
|
|
|
|
|
|
if not Array1.Count() = Array2.Count()
|
|
|
|
return false
|
|
|
|
end if
|
|
|
|
|
|
|
|
for each key in Array1
|
|
|
|
if not Array2.DoesExist(key)
|
|
|
|
return false
|
|
|
|
end if
|
|
|
|
|
|
|
|
if Array1[key] <> Array2[key]
|
|
|
|
return false
|
|
|
|
end if
|
|
|
|
end for
|
|
|
|
|
|
|
|
return true
|
|
|
|
end function
|
|
|
|
|
|
|
|
' Search string array for search value. Return if it's found
|
|
|
|
function inArray(haystack, needle) as boolean
|
|
|
|
valueToFind = needle
|
|
|
|
|
|
|
|
if LCase(type(valueToFind)) <> "rostring" and LCase(type(valueToFind)) <> "string"
|
|
|
|
valueToFind = str(needle)
|
|
|
|
end if
|
|
|
|
|
|
|
|
valueToFind = lcase(valueToFind)
|
|
|
|
|
|
|
|
for each item in haystack
|
|
|
|
if lcase(item) = valueToFind then return true
|
|
|
|
end for
|
|
|
|
|
|
|
|
return false
|
|
|
|
end function
|
|
|
|
|
|
|
|
function toString(input) as string
|
|
|
|
if LCase(type(input)) = "rostring" or LCase(type(input)) = "string"
|
|
|
|
return input
|
|
|
|
end if
|
|
|
|
|
|
|
|
return str(input)
|
|
|
|
end function
|
|
|
|
|
2023-11-30 01:22:26 +00:00
|
|
|
'
|
|
|
|
' startLoadingSpinner: Start a loading spinner and attach it to the main JFScene.
|
|
|
|
' Displays an invisible ProgressDialog node by default to disable keypresses while loading.
|
|
|
|
'
|
|
|
|
' @param {boolean} [disableRemote=true]
|
|
|
|
sub startLoadingSpinner(disableRemote = true as boolean)
|
|
|
|
if not isValid(m.scene)
|
|
|
|
m.scene = m.top.getScene()
|
|
|
|
end if
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-11-30 01:22:26 +00:00
|
|
|
if not m.scene.isLoading
|
|
|
|
m.scene.disableRemote = disableRemote
|
|
|
|
m.scene.isLoading = true
|
|
|
|
end if
|
2023-10-06 03:18:36 +00:00
|
|
|
end sub
|
|
|
|
|
|
|
|
sub stopLoadingSpinner()
|
2023-11-30 01:22:26 +00:00
|
|
|
if not isValid(m.scene)
|
|
|
|
m.scene = m.top.getScene()
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
2023-11-30 01:22:26 +00:00
|
|
|
|
|
|
|
if m.scene.isLoading
|
|
|
|
m.scene.disableRemote = false
|
|
|
|
m.scene.isLoading = false
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
|
|
|
end sub
|
|
|
|
|
2023-10-31 21:36:19 +00:00
|
|
|
' accepts the raw json string of /system/info/public and returns
|
|
|
|
' a boolean indicating if ProductName is "Jellyfin Server"
|
|
|
|
function isJellyfinServer(systemInfo as object) as boolean
|
|
|
|
data = ParseJson(systemInfo)
|
|
|
|
if isValid(data) and isValid(data.ProductName)
|
|
|
|
return LCase(data.ProductName) = m.global.constants.jellyfin_server
|
|
|
|
end if
|
|
|
|
return false
|
|
|
|
end function
|
|
|
|
|
2023-10-06 03:18:36 +00:00
|
|
|
' Check if a specific value is inside of an array
|
|
|
|
function arrayHasValue(arr as object, value as dynamic) as boolean
|
|
|
|
for each entry in arr
|
|
|
|
if entry = value
|
|
|
|
return true
|
|
|
|
end if
|
|
|
|
end for
|
|
|
|
return false
|
|
|
|
end function
|
2023-10-28 21:26:12 +00:00
|
|
|
|
|
|
|
' Takes an array of data, shuffles the order, then returns the array
|
|
|
|
' uses the Fisher-Yates shuffling algorithm
|
|
|
|
function shuffleArray(array as object) as object
|
|
|
|
for i = array.count() - 1 to 1 step -1
|
|
|
|
j = Rnd(i + 1) - 1
|
|
|
|
t = array[i] : array[i] = array[j] : array[j] = t
|
|
|
|
end for
|
|
|
|
return array
|
|
|
|
end function
|
2023-12-05 16:56:00 +00:00
|
|
|
</code></pre></article></section><footer class="footer" id="PeOAagUepe"><div class="wrapper"><span class="jsdoc-message">Automatically generated using <a href="https://github.com/jsdoc/jsdoc" target="_blank">JSDoc</a> and the <a href="https://github.com/ankitskvmdam/clean-jsdoc-theme" target="_blank">clean-jsdoc-theme</a>.</span></div></footer></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><a href="/" class="sidebar-title sidebar-title-anchor">jellyfin-roku Code Documentation</a><div class="mobile-nav-links"><div class="external-link navbar-item"><a id="jellyfin-link-mobile" href="https://jellyfin.org/" target="_blank">Jellyfin</a></div><div class="external-link navbar-item"><a id="github-link-mobile" href="https://github.com/jellyfin/jellyfin-roku" target="_blank">GitHub</a></div><div class="external-link navbar-item"><a id="forum-link-mobile" href="https://forum.jellyfin.org/f-roku-development" target="_blank">Forum</a></div><div class="external-link navbar-item"><a id="matrix-link-mobile" href="https://matrix.to/#/#jellyfin-dev-roku:matrix.org" target="_blank">Matrix</a></div></div><div class="mobile-sidebar-items-c"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-AlbumData.html">AlbumData</a></div><div class="sidebar-section-children"><a href="module-AlbumGrid.html">AlbumGrid</a></div><div class="sidebar-section-children"><a href="module-AlbumTrackList.html">AlbumTrackList</a></div><div class="sidebar-section-children"><a href="module-AlbumView.html">AlbumView</a></div><div class="sidebar-section-children"><a href="module-Alpha.html">Alpha</a></div><div class="sidebar-section-children"><a href="module-ArtistView.html">ArtistView</a></div><div class="sidebar-section-children"><a href="module-AudioPlayer.html">AudioPlayer</a></div><div class="sidebar-section-children"><a href="module-AudioPlayerView.html">AudioPlayerView</a></div><div class="sidebar-section-children"><a href="module-AudioTrackListItem.html">AudioTrackListItem</a></div><div class="sidebar-section-children"><a href="module-ButtonGroupHoriz.html">ButtonGroupHoriz</a></div><div class="sidebar-section-children"><a href="module-ButtonGroupVert.html">ButtonGroupVert</a></div><div class="sidebar-section-children"><a href="module-ChannelData.html">ChannelData</a></div><div class="sidebar-section-children"><a href="module-Clock.html">Clock</a></div><div class="sidebar-section-children"><a href="module-CollectionData.html">CollectionData</a></div><div class="sidebar-section-children"><a href="module-ConfigData.html">ConfigData</a></div><div class="sidebar-section-children"><a href="module-ConfigItem.html">ConfigItem</a></div><div class="sidebar-section-children"><a href="module-ConfigList.html">ConfigList</a></div><div class="sidebar-section-children"><a href="module-ExtrasItem.html">ExtrasItem</a></div><div class="sidebar-section-children"><a href="module-ExtrasRowList.html">ExtrasRowList</a></div><div class="sidebar-section-children"><a href="module-FavoriteItemsTask.html">FavoriteItemsTask</a></div><div class="sidebar-section-children"><a href="module-Folder
|