Merge pull request #465 from TwitchBronBron/ssdp-scan

This commit is contained in:
Neil Burrows 2021-07-18 14:51:34 +01:00 committed by GitHub
commit 312a1ce818
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 547 additions and 209 deletions

24
components/JFButton.brs Normal file
View File

@ -0,0 +1,24 @@
sub init()
m.top.observeFieldScoped("text", "onTextChanged")
m.top.iconUri = ""
m.top.focusedIconUri = ""
m.top.showFocusFootprint = true
m.top.minWidth = 0
end sub
'
' Whenever the text changes, pad both sides with whitespace so we can center the button text
'
sub onTextChanged()
addSpaceAfter = true
minChars = m.top.minChars
if minChars = invalid then minChars = 50
while m.top.text.Len() < minChars
if addSpaceAfter
m.top.text = m.top.text + Chr(160)
else
m.top.text = Chr(160) + m.top.text
end if
addSpaceAfter = addSpaceAfter = false
end while
end sub

7
components/JFButton.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="JFButton" extends="Button">
<interface>
<field id="minChars" type="int" />
</interface>
<script type="text/brightscript" uri="JFButton.brs" />
</component>

6
components/Spinner.brs Normal file
View File

@ -0,0 +1,6 @@
sub init()
m.top.poster.uri = "pkg:/images/spinner.png"
m.top.control = "start"
m.top.clockwise = true
m.top.spinInterval = 2
end sub

4
components/Spinner.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="Spinner" extends="BusySpinner">
<script type="text/brightscript" uri="Spinner.brs" />
</component>

View File

@ -0,0 +1,35 @@
sub init() as void
m.poster = m.top.findNode("poster")
m.name = m.top.findNode("name")
m.baseUrl = m.top.findNode("baseUrl")
m.labels = m.top.findNode("labels")
setTextColor(0)
end sub
sub itemContentChanged() as void
server = m.top.itemContent
m.poster.uri = server.iconUrl
m.name.text = server.name
m.baseUrl.text = server.baseUrl
end sub
sub onFocusPercentChange(event)
'print "focusPercentChange: " ; event.getData()
setTextColor(event.getData())
end sub
sub setTextColor(percentFocused)
white = "0xffffffff"
black = "0x00000099"
if percentFocused > .4
color = black
else
color = white
end if
children = m.labels.getChildren(-1, 0)
for each child in children
child.color = color
end for
end sub

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<component name="JFServer" extends="Group">
<interface>
<field id="itemContent" type="node" onchange="itemContentChanged" />
<field id="focusPercent" type="float" onchange="onFocusPercentChange" />
</interface>
<script type="text/brightscript" uri="JFServer.brs" />
<children>
<Poster id="poster" translation="[0,5]" width="90" height="90" />
<Group id="labels" translation="[100,0]">
<Label text="Name:" horizAlign="left" font="font:MediumBoldSystemFont" width="130" height="65" translation="[0,5]" />
<Label text="URL:" horizAlign="left" font="font:MediumBoldSystemFont" width="130" height="65" translation="[0,55]" />
<Label id="name" horizAlign="left" font="font:MediumSystemFont" height="65" translation="[125,5]" />
<Label id="baseUrl" horizAlign="left" font="font:MediumSystemFont" height="65" translation="[125,55]" />
</Group>
</children>
</component>

View File

@ -0,0 +1,168 @@
'
' Task used to discover jellyfin servers on the local network
'
sub init()
m.top.functionName = "execute"
end sub
sub execute()
m.servers = []
m.serverUrlMap = {}
m.locationUrlMap = {}
'send both requests at the same time
SendSSDPBroadcast()
SendClientDiscoveryBroadcast()
ts = CreateObject("roTimespan")
maxTimeMs = 2200
'monitor each port and collect messages
while True
elapsed = ts.TotalMilliseconds()
if elapsed >= maxTimeMs
exit while
end if
msg = Wait(100, m.ssdp.port)
if msg <> invalid
ProcessSSDPResponse(msg)
end if
msg = Wait(100, m.clientDiscovery.port)
if msg <> invalid
ProcessClientDiscoveryResponse(msg)
end if
end while
m.top.content = m.servers
print m.servers[0], m.servers[1], m.servers[2]
end sub
sub AddServer(server)
if m.serverUrlMap[server.baseUrl] = invalid
m.serverUrlMap[server.baseUrl] = true
m.servers.push(server)
end if
end sub
sub SendClientDiscoveryBroadcast()
m.clientDiscovery = {
port: CreateObject("roMessagePort"),
address: CreateObject("roSocketAddress"),
socket: CreateObject("roDatagramSocket"),
urlTransfer: CreateObject("roUrlTransfer")
}
m.clientDiscovery.address.SetAddress("255.255.255.255:7359")
m.clientDiscovery.urlTransfer.SetPort(m.clientDiscoveryPort)
m.clientDiscovery.socket.SetMessagePort(m.clientDiscovery.port)
m.clientDiscovery.socket.SetSendToAddress(m.clientDiscovery.address)
m.clientDiscovery.socket.NotifyReadable(true)
m.clientDiscovery.socket.SetBroadcast(true)
m.clientDiscovery.socket.SendStr("Who is JellyfinServer?")
end sub
sub ProcessClientDiscoveryResponse(message)
if Type(message) = "roSocketEvent" and message.GetSocketId() = m.clientDiscovery.socket.GetId() and m.clientDiscovery.socket.IsReadable()
try
responseJson = m.clientDiscovery.socket.ReceiveStr(4096)
server = ParseJson(responseJson)
AddServer({
name: server.Name,
baseUrl: server.Address,
'hardcoded icon since this service doesn't include them
iconUrl: "pkg:/images/logo-icon120.jpg",
iconWidth: 120,
iconHeight: 120
})
print "Found Jellyfin server using client discovery at " + server.Address
catch e
print "Error scanning for jellyfin server", message
end try
end if
end sub
sub SendSSDPBroadcast()
m.ssdp = {
port: CreateObject("roMessagePort"),
address: CreateObject("roSocketAddress"),
socket: CreateObject("roDatagramSocket"),
urlTransfer: CreateObject("roUrlTransfer")
}
m.ssdp.address.SetAddress("239.255.255.250:1900")
m.ssdp.socket.SetMessagePort(m.ssdp.port)
m.ssdp.socket.SetSendToAddress(m.ssdp.address)
m.ssdp.socket.NotifyReadable(true)
m.ssdp.urlTransfer.SetPort(m.ssdp.port)
'brightscript can't escape characters in strings, so create a few vars here so we can use them in the strings below
Q = Chr(34)
CRLF = Chr(13) + Chr(10)
ssdpStr = "M-SEARCH * HTTP/1.1" + CRLF
ssdpStr += "HOST: 239.255.255.250:1900" + CRLF
ssdpStr += "MAN: " + Q + "ssdp:discover" + Q + CRLF
ssdpStr += "ST:urn:schemas-upnp-org:device:MediaServer:1" + CRLF
ssdpStr += "MX: 2" + CRLF
ssdpStr += CRLF
m.ssdp.socket.SendStr(ssdpStr)
end sub
sub ProcessSSDPResponse(message)
locationUrl = invalid
if Type (message) = "roSocketEvent" and message.GetSocketId() = m.ssdp.socket.GetId() and m.ssdp.socket.IsReadable()
recvStr = m.ssdp.socket.ReceiveStr(4096)
match = CreateObject("roRegex", "\r\nLocation:\s*(.*?)\s*\r\n", "i").Match(recvStr)
if match.Count() = 2
locationUrl = match[1]
end if
end if
if locationUrl = invalid
return
else if m.locationUrlMap[locationUrl] <> invalid
print "Already discovered this location " + locationUrl
return
end if
m.locationUrlMap[locationUrl] = true
http = CreateObject("roUrlTransfer")
http.SetUrl(locationUrl)
responseText = http.GetToString()
xml = CreateObject("roXMLElement")
'if we successfully parsed the response, process it
if xml.Parse(responseText)
deviceNode = xml.GetNamedElementsCi("device")[0]
manufacturer = deviceNode.GetNamedElementsCi("manufacturer").GetText()
'only process jellyfin servers
if lcase(manufacturer) = "jellyfin"
'find the largest icon
width = 0
server = invalid
icons = deviceNode.GetNamedElementsCi("iconList")[0].GetNamedElementsCi("icon")
for each iconNode in icons
iconUrl = iconNode.GetNamedElementsCi("url").GetText()
baseUrl = invalid
match = CreateObject("roRegex", "(.*?)\/dlna\/", "i").Match(iconUrl)
if match.Count() = 2
baseUrl = match[1]
end if
loopResult = {
name: deviceNode.GetNamedElementsCi("friendlyName").GetText(),
baseUrl: baseUrl,
iconUrl: iconUrl,
iconWidth: iconNode.GetNamedElementsCi("width")[0].GetText().ToInt(),
iconHeight: iconNode.GetNamedElementsCi("height")[0].GetText().ToInt()
}
if baseUrl <> invalid and loopResult.iconWidth > width
width = loopResult.iconWidth
server = loopResult
end if
end for
AddServer(server)
print "Found jellyfin server using SSDP and DLNA at " + server.baseUrl
end if
end if
end sub

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<component name="ServerDiscoveryTask" extends="Task">
<interface>
<field id="content" type="array" />
</interface>
<script type="text/brightscript" uri="ServerDiscoveryTask.brs" />
</component>

View File

@ -0,0 +1,111 @@
sub init()
m.top.setFocus(true)
m.top.optionsAvailable = false
m.spinner = m.top.findNode("spinner")
m.serverPicker = m.top.findNode("serverPicker")
m.serverUrlTextbox = m.top.findNode("serverUrlTextbox")
m.serverUrlContainer = m.top.findNode("serverUrlContainer")
m.serverUrlOutline = m.top.findNode("serverUrlOutline")
m.submit = m.top.findNode("submit")
m.top.observeField("serverUrl", "clearErrorMessage")
ScanForServers()
end sub
function onKeyEvent(key as string, press as boolean) as boolean
print "onKeyEvent", key, press
if not press then return true
handled = true
if key = "OK" and m.serverPicker.hasFocus()
m.top.serverUrl = m.serverPicker.content.getChild(m.serverPicker.itemFocused).baseUrl
m.submit.setFocus(true)
'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
else if key = "down" and m.serverPicker.hasFocus() and m.serverPicker.content.getChildCount() > 0 and m.serverPicker.itemFocused = m.serverPicker.content.getChildCount() - 1
m.serverUrlContainer.setFocus(true)
'user navigating up to the server picker from the input box (it's only focusable if it has items)
else if key = "up" and m.serverUrlContainer.hasFocus() and m.servers.Count() > 0
m.serverPicker.setFocus(true)
else if key = "OK" and m.serverUrlContainer.hasFocus()
ShowKeyboard()
'focus the serverUrl input from submit button
else if key = "up" and m.submit.hasFocus()
m.serverUrlContainer.setFocus(true)
'focus the submit button from serverUrl
else if key = "down" and m.serverUrlContainer.hasFocus()
m.submit.setFocus(true)
else
handled = false
end if
'show/hide input box outline
m.serverUrlOutline.visible = m.serverUrlContainer.isInFocusChain()
return handled
end function
sub ScanForServers()
m.ssdpScanner = CreateObject("roSGNode", "ServerDiscoveryTask")
'run the task
m.ssdpScanner.observeField("content", "ScanForServersComplete")
m.ssdpScanner.control = "RUN"
end sub
sub ScanForServersComplete(event)
m.servers = event.getData()
items = CreateObject("roSGNode", "ContentNode")
for each server in m.servers
server.subtype = "ContentNode"
'add new fields for every server property onto the ContentNode (rather than making a dedicated component just to hold data...)
items.update([server], true)
end for
m.serverPicker.content = items
m.spinner.visible = false
'if we have at least one server, focus on the server picker
if m.servers.Count() > 0
m.serverPicker.setFocus(true)
'no servers found...focus on the input textbox
else
m.serverUrlContainer.setFocus(true)
'show/hide input box outline
m.serverUrlOutline.visible = true
end if
end sub
sub ShowKeyboard()
dialog = createObject("roSGNode", "KeyboardDialog")
dialog.title = tr("Enter the server name or ip address")
dialog.buttons = [tr("OK"), tr("Cancel")]
dialog.text = m.serverUrlTextbox.text
m.top.getscene().dialog = dialog
m.dialog = dialog
dialog.observeField("buttonSelected", "onDialogButton")
end sub
function onDialogButton()
d = m.dialog
button_text = d.buttons[d.buttonSelected]
if button_text = tr("OK")
m.serverUrlTextbox.text = d.text
m.dialog.close = true
return true
else if button_text = tr("Cancel")
m.dialog.close = true
return true
else
return false
end if
end function
sub clearErrorMessage()
m.top.errorMessage = ""
end sub

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<component name="SetServerScreen" extends="JFGroup">
<interface>
<field id="serverUrl" type="string" alias="serverUrlTextbox.text" />
<field id="serverWidth" alias="serverUrlOutline.width,serverUrlTextbox.width,serverUrlContainer.width,submitSizer.width" value="1620" />
<field id="serverHeight" alias="serverUrlOutline.height,serverUrlTextbox.height,serverUrlContainer.height" value="60" />
<field id="errorMessage" type="string" alias="errorMessage.text"/>
</interface>
<children>
<LayoutGroup translation="[150,150]" itemSpacings="40">
<LayoutGroup>
<label text="Connect to Server" id="prompt" font="font:LargeBoldSystemFont" />
<label text="Pick a Jellyfin server from the local network" />
</LayoutGroup>
<!--background for server picker-->
<Rectangle color="0x00000020" width="1620" height="400">
<Spinner id="spinner" translation="[717, 136]" />
<MarkupList id="serverPicker" translation="[50, 20]" itemComponentName="JFServer" itemSpacing="[0, 10]" itemSize="[1520, 100]" numRows="3" vertFocusAnimationStyle="floatingFocus" />
</Rectangle>
<label text="...or enter server URL manually:" translation="[0, 690]" />
<Rectangle id="serverUrlContainer" color="0x00000000">
<TextEditBox id="serverUrlTextbox" hintText="e.g. 192.168.1.100:8096 or https://example.com/jellyfin"></TextEditBox>
<Poster id="serverUrlOutline" visible="false" uri="pkg:/images/hd_focus.9.png" />
</Rectangle>
<label id="errorMessage" text="" font="font:MediumSystemFont" color="#ff0000FF" />
<LayoutGroup horizAlignment="center">
<JFButton id="submit" minChars="30" text="Submit"></JFButton>
<!--add a known width invisibile element to allow the button to be centered-->
<Rectangle id="submitSizer" width="1620" height="0" color="#00000000" />
</LayoutGroup>
</LayoutGroup>
</children>
<script type="text/brightscript" uri="SetServerScreen.brs" />
</component>

BIN
images/fhd_focus.9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

BIN
images/hd_focus.9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

BIN
images/logo-icon120.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
images/spinner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -414,5 +414,20 @@
<translation>The requested content does not exist on the server</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Enter the server name or ip address</source>
<translation>Enter the server name or ip address</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Pick a Jellyfin server from the local network</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>...or enter server URL manually:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
</context>
</TS>

179
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "jellyfin-roku",
"version": "1.4.8",
"version": "1.4.9",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -258,6 +258,15 @@
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
@ -851,15 +860,17 @@
}
},
"brighterscript": {
"version": "0.30.9",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.30.9.tgz",
"integrity": "sha512-4Raf4Mjdzi6D+14UVtIuW4r5RZaS5uli5AWzGZhfH87pd4jP89dJgANqxrstA5Pseo7xGPg0QxVKoAC13icayg==",
"version": "0.39.4",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.39.4.tgz",
"integrity": "sha512-VJR+6A+bMyRu4Fd+5xlSFAVSv4NtqNpkROZuS9x7pBGRqAjy6BBPjmSPOeRjK4TGv9CvaH3OD5ZYtsi2kaY1IA==",
"dev": true,
"requires": {
"@rokucommunity/bslib": "^0.1.1",
"@xml-tools/parser": "^1.0.7",
"array-flat-polyfill": "^1.0.1",
"chalk": "^2.4.2",
"chokidar": "^3.0.2",
"chevrotain": "^7.0.1",
"chokidar": "^3.5.1",
"clear": "^0.1.0",
"cross-platform-clear-console": "^2.3.0",
"debounce-promise": "^3.1.0",
@ -874,23 +885,17 @@
"moment": "^2.23.0",
"p-settle": "^2.1.0",
"parse-ms": "^2.1.0",
"roku-deploy": "^3.2.4",
"roku-deploy": "^3.4.1",
"serialize-error": "^7.0.1",
"source-map": "^0.7.3",
"vscode-languageserver": "^6.1.1",
"vscode-languageserver-protocol": "~3.15.3",
"vscode-languageserver": "7.0.0",
"vscode-languageserver-protocol": "3.16.0",
"vscode-languageserver-textdocument": "^1.0.1",
"vscode-uri": "^2.1.1",
"xml2js": "^0.4.19",
"yargs": "^15.4.0"
"yargs": "^16.2.0"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -937,38 +942,21 @@
}
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
"readdirp": "~3.6.0"
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -1054,15 +1042,6 @@
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -1081,101 +1060,19 @@
"is-number": "^7.0.0"
}
},
"vscode-jsonrpc": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==",
"dev": true
},
"vscode-languageserver": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz",
"integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==",
"dev": true,
"requires": {
"vscode-languageserver-protocol": "^3.15.3"
}
},
"vscode-languageserver-protocol": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
"dev": true,
"requires": {
"vscode-jsonrpc": "^5.0.1",
"vscode-languageserver-types": "3.15.1"
}
},
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==",
"dev": true
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
}
}
},
"y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true
},
"yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"requires": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
}
},
"yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
}
}
@ -3716,9 +3613,9 @@
}
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"

View File

@ -8,7 +8,7 @@
},
"devDependencies": {
"@rokucommunity/bslint": "^0.4.0",
"brighterscript": "^0.30.9",
"brighterscript": "^0.39.4",
"rooibos-cli": "^1.0.1"
},
"scripts": {

View File

@ -1,5 +1,5 @@
sub Main (args as Dynamic) as Void
' If the Rooibos files are included in deployment, run tests
'bs:disable-next-line
if type(Rooibos__Init) = "Function" then Rooibos__Init()
@ -21,7 +21,7 @@ sub Main (args as Dynamic) as Void
m.overhang = CreateObject("roSGNode", "JFOverhang")
m.scene.insertChild(m.overhang, 0)
app_start:
m.overhang.title = ""
' First thing to do is validate the ability to use the API
@ -64,7 +64,7 @@ sub Main (args as Dynamic) as Void
group.control = "play"
ReportPlayback(group, "start")
m.overhang.visible = false
else
else
dialog = createObject("roSGNode", "Dialog")
dialog.id = "OKDialog"
dialog.title = tr("Not found")
@ -197,7 +197,7 @@ sub Main (args as Dynamic) as Void
group.control = "play"
ReportPlayback(group, "start")
m.overhang.visible = false
else
else
dialog = createObject("roSGNode", "Dialog")
dialog.id = "OKDialog"
dialog.title = tr("Error loading Channel Data")
@ -344,7 +344,7 @@ sub Main (args as Dynamic) as Void
movie.favorite = not movie.favorite
else
' If there are no other button matches, check if this is a simple "OK" Dialog & Close if so
dialog = msg.getRoSGNode()
dialog = msg.getRoSGNode()
if dialog.id = "OKDialog"
dialog.unobserveField("buttonSelected")
dialog.close = true
@ -433,7 +433,7 @@ sub Main (args as Dynamic) as Void
group.control = "play"
ReportPlayback(group, "start")
m.overhang.visible = false
else
else
dialog = createObject("roSGNode", "Dialog")
dialog.id = "OKDialog"
dialog.title = tr("Not found")
@ -607,4 +607,3 @@ sub SendPerformanceBeacon(signalName as string)
m.scene.signalBeacon(signalName)
end if
end sub

View File

@ -1,30 +1,20 @@
function CreateServerGroup()
' Get and Save Jellyfin Server Information
group = CreateObject("roSGNode", "ConfigScene")
m.scene.appendChild(group)
port = CreateObject("roMessagePort")
group.findNode("prompt").text = tr("Connect to Server")
screen = CreateObject("roSGNode", "SetServerScreen")
m.scene.appendChild(screen)
port = CreateObject("roMessagePort")
m.colors = {}
config = group.findNode("configOptions")
server_field = CreateObject("roSGNode", "ConfigData")
server_field.label = tr("Server")
server_field.field = "server"
server_field.type = "string"
if get_setting("server") <> invalid
server_field.value = get_setting("server")
screen.serverUrl = get_setting("server")
end if
group.findNode("example").text = tr("192.168.1.100:8096 or https://example.com/jellyfin")
items = [ server_field ]
config.configItems = items
button = group.findNode("submit")
m.viewModel = {}
button = screen.findNode("submit")
button.observeField("buttonSelected", port)
server_hostname = config.content.getChild(0)
group.observeField("backPressed", port)
screen.observeField("backPressed", port)
while true
msg = wait(0, port)
print type(msg), msg
if type(msg) = "roSGScreenEvent" and msg.isScreenClosed()
return "false"
else if isNodeEvent(msg, "backPressed")
@ -32,28 +22,13 @@ function CreateServerGroup()
else if type(msg) = "roSGNodeEvent"
node = msg.getNode()
if node = "submit"
'Append default ports
maxSlashes = 0
if left(lcase(server_hostname.value),8) = "https://" or left(lcase(server_hostname.value),7) = "http://" then maxSlashes = 2
'Check to make sure entry has no extra slashes before adding default ports.
if Instr(0, server_hostname.value, "/") = maxSlashes
if server_hostname.value.len() > 5 and mid(server_hostname.value, server_hostname.value.len()-4,1) <> ":" and mid(server_hostname.value, server_hostname.value.len()-5,1) <> ":"
if left(lcase(server_hostname.value) ,5) = "https"
server_hostname.value = server_hostname.value + ":8920"
else
server_hostname.value = server_hostname.value + ":8096"
end if
end if
end if
'Append http:// to server
if left(lcase(server_hostname.value),4) <> "http" then server_hostname.value = "http://" + server_hostname.value
serverUrl = standardize_jellyfin_url(screen.serverUrl)
'If this is a different server from what we know, reset username/password setting
if get_setting("server") <> server_hostname.value
if get_setting("server") <> serverUrl
set_setting("username", "")
set_setting("password", "")
end if
set_setting("server", server_hostname.value)
set_setting("server", serverUrl)
' Show Connecting to Server spinner
dialog = createObject("roSGNode", "ProgressDialog")
dialog.title = tr("Connecting to Server")
@ -67,30 +42,31 @@ function CreateServerGroup()
' Maybe don't unset setting, but offer as a prompt
' Server not found, is it online? New values / Retry
print "Server not found, is it online? New values / Retry"
group.findNode("alert").text = tr("Server not found, is it online?")
screen.errorMessage = tr("Server not found, is it online?")
SignOut()
else if serverInfoResult.Error <> invalid and serverInfoResult.Error
' If server redirected received, update the URL
if serverInfoResult.UpdatedUrl <> invalid
server_hostname.value = serverInfoResult.UpdatedUrl
serverUrl = serverInfoResult.UpdatedUrl
set_setting("server", serverUrl)
end if
' Display Error Message to user
message = tr("Error: ")
if serverInfoResult.ErrorCode <> invalid
message = message + "[" + serverInfoResult.ErrorCode.toStr() + "] "
end if
group.findNode("alert").text = message + tr(serverInfoResult.ErrorMessage)
screen.errorMessage = message + tr(serverInfoResult.ErrorMessage)
SignOut()
else
group.visible = false
screen.visible = false
return "true"
endif
end if
end if
end if
end while
' Just hide it when done, in case we need to come back
group.visible = false
screen.visible = false
return ""
end function
@ -100,7 +76,7 @@ function CreateUserSelectGroup(users = [])
end if
group = CreateObject("roSGNode", "UserSelect")
m.scene.appendChild(group)
port = CreateObject("roMessagePort")
port = CreateObject("roMessagePort")
group.itemContent = users
group.findNode("userRow").observeField("userSelected", port)
@ -131,7 +107,7 @@ function CreateSigninGroup(user = "")
' Get and Save Jellyfin user login credentials
group = CreateObject("roSGNode", "ConfigScene")
m.scene.appendChild(group)
port = CreateObject("roMessagePort")
port = CreateObject("roMessagePort")
group.findNode("prompt").text = tr("Sign In")
@ -152,7 +128,7 @@ function CreateSigninGroup(user = "")
if get_setting("password") <> invalid
password_field.value = get_setting("password")
end if
items = [ username_field, password_field ]
items = [username_field, password_field]
config.configItems = items
button = group.findNode("submit")
@ -206,9 +182,9 @@ function CreateHomeGroup()
sidepanel.observeField("closeSidePanel", m.port)
new_options = []
options_buttons = [
{"title": "Search", "id": "goto_search"},
{"title": "Change server", "id": "change_server"},
{"title": "Sign out", "id": "sign_out"}
{ "title": "Search", "id": "goto_search" },
{ "title": "Change server", "id": "change_server" },
{ "title": "Sign out", "id": "sign_out" }
]
for each opt in options_buttons
o = CreateObject("roSGNode", "OptionsButton")
@ -225,7 +201,7 @@ function CreateHomeGroup()
user_node.base_title = tr("Profile")
user_options = []
for each user in AvailableUsers()
user_options.push({display: user.username + "@" + user.server, value: user.id})
user_options.push({ display: user.username + "@" + user.server, value: user.id })
end for
user_node.choices = user_options
user_node.value = get_setting("active_user")

View File

@ -31,11 +31,11 @@ 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()
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=""
r = ""
if val(hours) > 0 then r = hours + ":"
r = r + minutes + ":" + seconds
return r
@ -70,10 +70,10 @@ 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
if int(a / b) = a / b
return a / b
end if
return a/b + 1
return a / b + 1
end function
'Returns the item selected or -1 on backpress or other unhandled closure of dialog.
@ -83,7 +83,7 @@ function get_dialog_result(dialog, port)
if isNodeEvent(msg, "backPressed")
return -1
else if isNodeEvent(msg, "itemSelected")
return dialog.findNode("optionList").itemSelected
return dialog.findNode("optionList").itemSelected
end if
end while
'Dialog has closed outside of this loop, return -1 for failure
@ -95,8 +95,8 @@ function lastFocusedChild(obj as object) as object
for i = 0 to obj.getChildCount()
if obj.focusedChild <> invalid
child = child.focusedChild
end if
end for
end if
end for
return child
end function
@ -137,9 +137,36 @@ function show_dialog(message as string, options = [], defaultSelection = 0) as i
end function
function message_dialog(message = "" as string)
return show_dialog(message,["OK"])
return show_dialog(message, ["OK"])
end function
function option_dialog(options, message = "", defaultSelection = 0) as integer
return show_dialog(message, options, defaultSelection)
end function
'
' Take a jellyfin hostname and ensure it's a full url.
' prepend http or https and append default ports, and remove excess slashes
'
function standardize_jellyfin_url(url as string)
'Append default ports
maxSlashes = 0
if left(url, 8) = "https://" or left(url, 7) = "http://"
maxSlashes = 2
end if
'Check to make sure entry has no extra slashes before adding default ports.
if Instr(0, url, "/") = maxSlashes
if url.len() > 5 and mid(url, url.len() - 4, 1) <> ":" and mid(url, url.len() - 5, 1) <> ":"
if left(url, 5) = "https"
url = url + ":8920"
else
url = url + ":8096"
end if
end if
end if
'Append http:// to server
if left(url, 4) <> "http"
url = "http://" + url
end if
return url
end function