From e03f965758ccdabcbd44280ced3dca8ffaecc229 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Tue, 12 Sep 2023 18:51:42 -0400 Subject: [PATCH 01/13] create generic task for posting data. wait for response code --- components/tasks/PostTask.bs | 39 +++++++++++++++++++++++++++++++++++ components/tasks/PostTask.xml | 11 ++++++++++ 2 files changed, 50 insertions(+) create mode 100644 components/tasks/PostTask.bs create mode 100644 components/tasks/PostTask.xml diff --git a/components/tasks/PostTask.bs b/components/tasks/PostTask.bs new file mode 100644 index 000000000..e0149ccca --- /dev/null +++ b/components/tasks/PostTask.bs @@ -0,0 +1,39 @@ +import "pkg:/source/api/baserequest.brs" + +sub init() + m.top.functionName = "postItems" +end sub + +sub postItems() + if m.top.apiUrl <> "" + if m.top.arrayData.count() > 0 and m.top.stringData = "" + print "PostTask Started - " + m.top.apiUrl, m.top.arrayData + req = APIRequest(m.top.apiUrl) + req.SetRequest("POST") + httpResponse = jsonPostTask(req, FormatJson(m.top.arrayData)) + m.top.responseCode = httpResponse + print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() + else if m.top.arrayData.count() = 0 and m.top.stringData <> "" + print "PostTask Started - " + m.top.apiUrl + " Data= " + m.top.stringData + req = APIRequest(m.top.apiUrl) + req.SetRequest("POST") + httpResponse = jsonPostTask(req, m.top.stringData) + m.top.responseCode = httpResponse + print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() + else + print "ERROR processing data for PostTask" + end if + else + print "ERROR in PostTask. Invalid API URL provided" + end if +end sub + +' Post data and wait for response code +function jsonPostTask(req, data = "" as string) as integer + req.setMessagePort(CreateObject("roMessagePort")) + req.AddHeader("Content-Type", "application/json") + resp = req.PostFromString(data) + + return resp +end function + diff --git a/components/tasks/PostTask.xml b/components/tasks/PostTask.xml new file mode 100644 index 000000000..ee1580921 --- /dev/null +++ b/components/tasks/PostTask.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file From 060519810b68df5209b11c60286967437e546ac3 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Tue, 12 Sep 2023 18:53:25 -0400 Subject: [PATCH 02/13] use post task when posting device profile + post device profile when exiting the settings page --- components/data/SceneManager.brs | 33 ++++++++++++++++++++++++++--- components/data/SceneManager.xml | 1 + source/Main.brs | 6 +++--- source/utils/deviceCapabilities.brs | 8 ------- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/components/data/SceneManager.brs b/components/data/SceneManager.brs index e876957f5..85140a4d7 100755 --- a/components/data/SceneManager.brs +++ b/components/data/SceneManager.brs @@ -1,4 +1,5 @@ import "pkg:/source/roku_modules/log/LogMixin.brs" +import "pkg:/source/utils/deviceCapabilities.brs" sub init() m.log = log.Logger("SceneManager") @@ -6,6 +7,8 @@ sub init() m.scene = m.top.getScene() m.content = m.scene.findNode("content") m.overhang = m.scene.findNode("overhang") + m.postTask = CreateObject("roSGNode", "PostTask") + m.postTask.observeField("responseCode", "postFinished") end sub ' @@ -77,16 +80,21 @@ end sub sub popScene() group = m.groups.pop() if group <> invalid - if group.isSubType("JFGroup") + groupType = group.subtype() + + if groupType = "JFGroup" unregisterOverhangData(group) - else if group.isSubType("JFVideo") + else if groupType = "JFVideo" ' Stop video to make sure app communicates stop playstate to server group.control = "stop" + else if groupType = "Settings" + ' update device profile after exiting the settings page - some settings affect the device profile + postProfile() end if group.visible = false - if group.isSubType("JFScreen") + if groupType = "JFScreen" group.callFunc("OnScreenHidden") end if else @@ -355,3 +363,22 @@ end sub function isDialogOpen() as boolean return m.scene.dialog <> invalid end function + +' Send Device Profile information to server +function postProfile() as boolean + m.postTask.arrayData = getDeviceCapabilities() + m.postTask.apiUrl = "/Sessions/Capabilities/Full" + m.postTask.control = "RUN" + return true +end function + +' Return the Post Task to it's default state +sub postFinished() + m.postTask.unobserveField("responseCode") + ' Empty the Post Task data to its default state + m.postTask.apiUrl = "" + m.postTask.arrayData = {} + m.postTask.stringData = "" + m.postTask.responseCode = 0 + m.postTask.observeField("responseCode", "postFinished") +end sub diff --git a/components/data/SceneManager.xml b/components/data/SceneManager.xml index 3cc43b648..b9ecf60b5 100755 --- a/components/data/SceneManager.xml +++ b/components/data/SceneManager.xml @@ -14,6 +14,7 @@ + diff --git a/source/Main.brs b/source/Main.brs index 298650bd2..3a164518a 100644 --- a/source/Main.brs +++ b/source/Main.brs @@ -32,7 +32,7 @@ sub Main (args as dynamic) as void ' First thing to do is validate the ability to use the API if not LoginFlow() then return ' tell jellyfin server about device capabilities - PostDeviceProfile() + m.global.sceneManager.callFunc("postProfile") ' remove previous scenes from the stack sceneManager.callFunc("clearScenes") @@ -621,12 +621,12 @@ sub Main (args as dynamic) as void ' The audio codec capability has changed if true. print "event.audioCodecCapabilityChanged = ", event.audioCodecCapabilityChanged - PostDeviceProfile() + m.global.sceneManager.callFunc("postProfile") else if isValid(event.videoCodecCapabilityChanged) ' The video codec capability has changed if true. print "event.videoCodecCapabilityChanged = ", event.videoCodecCapabilityChanged - PostDeviceProfile() + m.global.sceneManager.callFunc("postProfile") else if isValid(event.appFocus) ' It is set to False when the System Overlay (such as the confirm partner button HUD or the caption control overlay) takes focus and True when the channel regains focus print "event.appFocus = ", event.appFocus diff --git a/source/utils/deviceCapabilities.brs b/source/utils/deviceCapabilities.brs index fcc256ce3..cf1dcc7ed 100644 --- a/source/utils/deviceCapabilities.brs +++ b/source/utils/deviceCapabilities.brs @@ -17,14 +17,6 @@ function getDeviceCapabilities() as object } end function -' Send Device Profile information to server -sub PostDeviceProfile() - body = getDeviceCapabilities() - req = APIRequest("/Sessions/Capabilities/Full") - req.SetRequest("POST") - postJson(req, FormatJson(body)) -end sub - function getDeviceProfile() as object playMpeg2 = m.global.session.user.settings["playback.mpeg2"] playAv1 = m.global.session.user.settings["playback.av1"] From 1058802118255ced3a502410ee04cb88a0faac14 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Tue, 12 Sep 2023 19:06:17 -0400 Subject: [PATCH 03/13] remove duplicate comment --- components/data/SceneManager.brs | 1 - 1 file changed, 1 deletion(-) diff --git a/components/data/SceneManager.brs b/components/data/SceneManager.brs index 85140a4d7..4a454cc41 100755 --- a/components/data/SceneManager.brs +++ b/components/data/SceneManager.brs @@ -375,7 +375,6 @@ end function ' Return the Post Task to it's default state sub postFinished() m.postTask.unobserveField("responseCode") - ' Empty the Post Task data to its default state m.postTask.apiUrl = "" m.postTask.arrayData = {} m.postTask.stringData = "" From 46f24293c4c51feb303ab37a7f7cd1af6d117c7d Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Tue, 12 Sep 2023 19:18:07 -0400 Subject: [PATCH 04/13] restore debugging info for device profile --- components/data/SceneManager.brs | 33 +++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/components/data/SceneManager.brs b/components/data/SceneManager.brs index 85140a4d7..3c3865836 100755 --- a/components/data/SceneManager.brs +++ b/components/data/SceneManager.brs @@ -366,7 +366,38 @@ end function ' Send Device Profile information to server function postProfile() as boolean - m.postTask.arrayData = getDeviceCapabilities() + profile = getDeviceCapabilities() + print "profile =", profile + print "profile.DeviceProfile =", profile.DeviceProfile + print "profile.DeviceProfile.CodecProfiles =" + for each prof in profile.DeviceProfile.CodecProfiles + print prof + for each cond in prof.Conditions + print cond + end for + end for + print "profile.DeviceProfile.ContainerProfiles =", profile.DeviceProfile.ContainerProfiles + print "profile.DeviceProfile.DirectPlayProfiles =" + for each prof in profile.DeviceProfile.DirectPlayProfiles + print prof + end for + print "profile.DeviceProfile.SubtitleProfiles =" + for each prof in profile.DeviceProfile.SubtitleProfiles + print prof + end for + print "profile.DeviceProfile.TranscodingProfiles =" + for each prof in profile.DeviceProfile.TranscodingProfiles + print prof + if isValid(prof.Conditions) + for each condition in prof.Conditions + print condition + end for + end if + end for + print "profile.PlayableMediaTypes =", profile.PlayableMediaTypes + print "profile.SupportedCommands =", profile.SupportedCommands + + m.postTask.arrayData = profile m.postTask.apiUrl = "/Sessions/Capabilities/Full" m.postTask.control = "RUN" return true From be9aa3813456ac65a1b588b7b01128665231ed00 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Sun, 17 Sep 2023 12:39:19 -0400 Subject: [PATCH 05/13] Use async with customizable timout + save response body, headers, and failure reason --- components/data/SceneManager.brs | 7 ++--- components/tasks/PostTask.bs | 49 +++++++++++++++++++++++++++----- components/tasks/PostTask.xml | 11 +++++-- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/components/data/SceneManager.brs b/components/data/SceneManager.brs index e620ff0ab..76d4e46bf 100755 --- a/components/data/SceneManager.brs +++ b/components/data/SceneManager.brs @@ -403,12 +403,9 @@ function postProfile() as boolean return true end function -' Return the Post Task to it's default state +' Post Task is finished sub postFinished() m.postTask.unobserveField("responseCode") - m.postTask.apiUrl = "" - m.postTask.arrayData = {} - m.postTask.stringData = "" - m.postTask.responseCode = 0 + m.postTask.callFunc("emptyPostTask") m.postTask.observeField("responseCode", "postFinished") end sub diff --git a/components/tasks/PostTask.bs b/components/tasks/PostTask.bs index e0149ccca..a42558240 100644 --- a/components/tasks/PostTask.bs +++ b/components/tasks/PostTask.bs @@ -7,17 +7,17 @@ end sub sub postItems() if m.top.apiUrl <> "" if m.top.arrayData.count() > 0 and m.top.stringData = "" - print "PostTask Started - " + m.top.apiUrl, m.top.arrayData + print "PostTask Started - Posting array to " + m.top.apiUrl req = APIRequest(m.top.apiUrl) req.SetRequest("POST") - httpResponse = jsonPostTask(req, FormatJson(m.top.arrayData)) + httpResponse = asyncPost(req, FormatJson(m.top.arrayData)) m.top.responseCode = httpResponse print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() else if m.top.arrayData.count() = 0 and m.top.stringData <> "" - print "PostTask Started - " + m.top.apiUrl + " Data= " + m.top.stringData + print "PostTask Started - Posting string(" + m.top.stringData + ") to " + m.top.apiUrl req = APIRequest(m.top.apiUrl) req.SetRequest("POST") - httpResponse = jsonPostTask(req, m.top.stringData) + httpResponse = asyncPost(req, m.top.stringData) m.top.responseCode = httpResponse print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() else @@ -29,11 +29,46 @@ sub postItems() end sub ' Post data and wait for response code -function jsonPostTask(req, data = "" as string) as integer +function asyncPost(req, data = "" as string) as integer + ' response code 0 means there was an error + respCode = 0 + req.setMessagePort(CreateObject("roMessagePort")) req.AddHeader("Content-Type", "application/json") - resp = req.PostFromString(data) + req.AsyncPostFromString(data) + ' wait up to m.top.timeoutSeconds for a response + ' NOTE: wait() uses milliseconds - multiply by 1000 to convert + resp = wait(m.top.timeoutSeconds * 1000, req.GetMessagePort()) + + respString = resp.GetString() + if respString <> invalid and respString <> "" + m.top.responseBody = ParseJson(respString) + print "m.top.responseBody=", m.top.responseBody + end if + + respCode = resp.GetResponseCode() + if respCode < 0 + ' there was an unexpected error + m.top.failureReason = resp.GetFailureReason() + else if respCode >= 200 and respCode < 300 + ' save response headers if they're available + m.top.responseHeaders = resp.GetResponseHeaders() + end if - return resp + return respCode end function +' Revert PostTask to default state +sub emptyPostTask() + ' These should match the defaults set in PostTask.xml + m.top.apiUrl = "" + m.top.timeoutSeconds = 30 + + m.top.arrayData = {} + m.top.stringData = "" + + m.top.responseCode = invalid + m.top.responseBody = {} + m.top.responseHeaders = {} + m.top.failureReason = "" +end sub diff --git a/components/tasks/PostTask.xml b/components/tasks/PostTask.xml index ee1580921..0188315e0 100644 --- a/components/tasks/PostTask.xml +++ b/components/tasks/PostTask.xml @@ -2,10 +2,17 @@ + + + - - + + + + + + \ No newline at end of file From a11f786d99a02a73224980e0872a571d52a35be3 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Wed, 20 Sep 2023 19:08:47 -0400 Subject: [PATCH 06/13] first attempt with promises --- components/data/SceneManager.brs | 51 +---------------- components/data/SceneManager.xml | 1 - components/tasks/PostTask.bs | 74 ------------------------- components/tasks/PostTask.xml | 18 ------ components/tasks/PromiseTask.bs | 27 +++++++++ components/tasks/PromiseTask.xml | 6 ++ components/tasks/RequestPromiseTask.bs | 6 ++ components/tasks/RequestPromiseTask.xml | 2 + package-lock.json | 13 +++++ package.json | 9 ++- source/Main.brs | 3 +- source/utils/deviceCapabilities.brs | 51 +++++++++++++++++ source/utils/session.bs | 3 + 13 files changed, 120 insertions(+), 144 deletions(-) delete mode 100644 components/tasks/PostTask.bs delete mode 100644 components/tasks/PostTask.xml create mode 100644 components/tasks/PromiseTask.bs create mode 100644 components/tasks/PromiseTask.xml create mode 100644 components/tasks/RequestPromiseTask.bs create mode 100644 components/tasks/RequestPromiseTask.xml diff --git a/components/data/SceneManager.brs b/components/data/SceneManager.brs index 76d4e46bf..61b2b6504 100755 --- a/components/data/SceneManager.brs +++ b/components/data/SceneManager.brs @@ -1,5 +1,6 @@ import "pkg:/source/roku_modules/log/LogMixin.brs" import "pkg:/source/utils/deviceCapabilities.brs" +import "pkg:/source/roku_modules/promises/promises.brs" sub init() m.log = log.Logger("SceneManager") @@ -7,8 +8,6 @@ sub init() m.scene = m.top.getScene() m.content = m.scene.findNode("content") m.overhang = m.scene.findNode("overhang") - m.postTask = CreateObject("roSGNode", "PostTask") - m.postTask.observeField("responseCode", "postFinished") end sub ' @@ -89,7 +88,7 @@ sub popScene() group.control = "stop" else if groupType = "Settings" ' update device profile after exiting the settings page - some settings affect the device profile - postProfile() + postDeviceProfile() end if group.visible = false @@ -363,49 +362,3 @@ end sub function isDialogOpen() as boolean return m.scene.dialog <> invalid end function - -' Send Device Profile information to server -function postProfile() as boolean - profile = getDeviceCapabilities() - print "profile =", profile - print "profile.DeviceProfile =", profile.DeviceProfile - print "profile.DeviceProfile.CodecProfiles =" - for each prof in profile.DeviceProfile.CodecProfiles - print prof - for each cond in prof.Conditions - print cond - end for - end for - print "profile.DeviceProfile.ContainerProfiles =", profile.DeviceProfile.ContainerProfiles - print "profile.DeviceProfile.DirectPlayProfiles =" - for each prof in profile.DeviceProfile.DirectPlayProfiles - print prof - end for - print "profile.DeviceProfile.SubtitleProfiles =" - for each prof in profile.DeviceProfile.SubtitleProfiles - print prof - end for - print "profile.DeviceProfile.TranscodingProfiles =" - for each prof in profile.DeviceProfile.TranscodingProfiles - print prof - if isValid(prof.Conditions) - for each condition in prof.Conditions - print condition - end for - end if - end for - print "profile.PlayableMediaTypes =", profile.PlayableMediaTypes - print "profile.SupportedCommands =", profile.SupportedCommands - - m.postTask.arrayData = profile - m.postTask.apiUrl = "/Sessions/Capabilities/Full" - m.postTask.control = "RUN" - return true -end function - -' Post Task is finished -sub postFinished() - m.postTask.unobserveField("responseCode") - m.postTask.callFunc("emptyPostTask") - m.postTask.observeField("responseCode", "postFinished") -end sub diff --git a/components/data/SceneManager.xml b/components/data/SceneManager.xml index b9ecf60b5..3cc43b648 100755 --- a/components/data/SceneManager.xml +++ b/components/data/SceneManager.xml @@ -14,7 +14,6 @@ - diff --git a/components/tasks/PostTask.bs b/components/tasks/PostTask.bs deleted file mode 100644 index a42558240..000000000 --- a/components/tasks/PostTask.bs +++ /dev/null @@ -1,74 +0,0 @@ -import "pkg:/source/api/baserequest.brs" - -sub init() - m.top.functionName = "postItems" -end sub - -sub postItems() - if m.top.apiUrl <> "" - if m.top.arrayData.count() > 0 and m.top.stringData = "" - print "PostTask Started - Posting array to " + m.top.apiUrl - req = APIRequest(m.top.apiUrl) - req.SetRequest("POST") - httpResponse = asyncPost(req, FormatJson(m.top.arrayData)) - m.top.responseCode = httpResponse - print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() - else if m.top.arrayData.count() = 0 and m.top.stringData <> "" - print "PostTask Started - Posting string(" + m.top.stringData + ") to " + m.top.apiUrl - req = APIRequest(m.top.apiUrl) - req.SetRequest("POST") - httpResponse = asyncPost(req, m.top.stringData) - m.top.responseCode = httpResponse - print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() - else - print "ERROR processing data for PostTask" - end if - else - print "ERROR in PostTask. Invalid API URL provided" - end if -end sub - -' Post data and wait for response code -function asyncPost(req, data = "" as string) as integer - ' response code 0 means there was an error - respCode = 0 - - req.setMessagePort(CreateObject("roMessagePort")) - req.AddHeader("Content-Type", "application/json") - req.AsyncPostFromString(data) - ' wait up to m.top.timeoutSeconds for a response - ' NOTE: wait() uses milliseconds - multiply by 1000 to convert - resp = wait(m.top.timeoutSeconds * 1000, req.GetMessagePort()) - - respString = resp.GetString() - if respString <> invalid and respString <> "" - m.top.responseBody = ParseJson(respString) - print "m.top.responseBody=", m.top.responseBody - end if - - respCode = resp.GetResponseCode() - if respCode < 0 - ' there was an unexpected error - m.top.failureReason = resp.GetFailureReason() - else if respCode >= 200 and respCode < 300 - ' save response headers if they're available - m.top.responseHeaders = resp.GetResponseHeaders() - end if - - return respCode -end function - -' Revert PostTask to default state -sub emptyPostTask() - ' These should match the defaults set in PostTask.xml - m.top.apiUrl = "" - m.top.timeoutSeconds = 30 - - m.top.arrayData = {} - m.top.stringData = "" - - m.top.responseCode = invalid - m.top.responseBody = {} - m.top.responseHeaders = {} - m.top.failureReason = "" -end sub diff --git a/components/tasks/PostTask.xml b/components/tasks/PostTask.xml deleted file mode 100644 index 0188315e0..000000000 --- a/components/tasks/PostTask.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/components/tasks/PromiseTask.bs b/components/tasks/PromiseTask.bs new file mode 100644 index 000000000..18872c8cb --- /dev/null +++ b/components/tasks/PromiseTask.bs @@ -0,0 +1,27 @@ +import "pkg:/source/roku_modules/promises/promises.brs" + +sub init() + m.top.functionName = "resolve" + m.input = invalid +end sub + +function getPromise(input = invalid as dynamic) as object + m.input = input + m.top.promise = promises.create() + m.top.control = "run" + return m.top.promise +end function + +' Override me +function exec(input = invalid as dynamic) as dynamic + throw "Not implemented" +end function + +sub resolve() + try + result = exec(m.input) + promises.resolve(result, m.top.promise) + catch e + promises.reject(e, m.top.promise) + end try +end sub diff --git a/components/tasks/PromiseTask.xml b/components/tasks/PromiseTask.xml new file mode 100644 index 000000000..3f66f4ded --- /dev/null +++ b/components/tasks/PromiseTask.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/components/tasks/RequestPromiseTask.bs b/components/tasks/RequestPromiseTask.bs new file mode 100644 index 000000000..299d52ed9 --- /dev/null +++ b/components/tasks/RequestPromiseTask.bs @@ -0,0 +1,6 @@ +import "pkg:/source/roku_modules/rokurequests/Requests.brs" + +function exec(input as object) as dynamic + print "input=", input + return Requests.request(input.method, input.url, input.params) +end function diff --git a/components/tasks/RequestPromiseTask.xml b/components/tasks/RequestPromiseTask.xml new file mode 100644 index 000000000..2529a7b4d --- /dev/null +++ b/components/tasks/RequestPromiseTask.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3f0ef5340..a4d0d8012 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "brighterscript-formatter": "1.6.32", "intKeyboard": "npm:integer-keyboard@1.0.12", "log": "npm:roku-log@0.11.1", + "promises": "npm:@rokucommunity/promises@0.1.0", + "roku-requests": "1.2.0", "sob": "npm:slide-out-button@1.0.1" }, "devDependencies": { @@ -3688,6 +3690,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promises": { + "name": "@rokucommunity/promises", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@rokucommunity/promises/-/promises-0.1.0.tgz", + "integrity": "sha512-FOPtEnHXfShwhrQiGSksTK0AQt5+rxcqpP6hg0U3Dx7WbMpL9PEd3udJhQteg3Iy2/GF858vYLcwdG8YdOloYw==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -4369,6 +4377,11 @@ "integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==", "dev": true }, + "node_modules/roku-requests": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/roku-requests/-/roku-requests-1.2.0.tgz", + "integrity": "sha512-X6XakmJwxT8H+YNvjZOwH+k8rmYpD5o47k37WyZJ/brrSD6cw9xbNdxO203nz6jpso4maQJGwSJvLGZpzSQ0AA==" + }, "node_modules/rooibos-roku": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/rooibos-roku/-/rooibos-roku-5.7.0.tgz", diff --git a/package.json b/package.json index 1412e72b6..89d43e1d9 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,15 @@ "brighterscript-formatter": "1.6.32", "intKeyboard": "npm:integer-keyboard@1.0.12", "log": "npm:roku-log@0.11.1", + "promises": "npm:@rokucommunity/promises@0.1.0", + "roku-requests": "1.2.0", "sob": "npm:slide-out-button@1.0.1" }, + "ropm": { + "noprefix": [ + "roku-requests" + ] +}, "devDependencies": { "@rokucommunity/bslint": "0.8.10", "brighterscript": "0.65.5", @@ -52,4 +59,4 @@ "url": "https://github.com/jellyfin/jellyfin-roku/issues" }, "homepage": "https://github.com/jellyfin/jellyfin-roku" -} \ No newline at end of file +} diff --git a/source/Main.brs b/source/Main.brs index 3a164518a..8a96c985e 100644 --- a/source/Main.brs +++ b/source/Main.brs @@ -29,10 +29,11 @@ sub Main (args as dynamic) as void m.global.addFields({ audioPlayer: CreateObject("roSGNode", "AudioPlayer") }) app_start: + postDeviceProfile() ' First thing to do is validate the ability to use the API if not LoginFlow() then return ' tell jellyfin server about device capabilities - m.global.sceneManager.callFunc("postProfile") + ' remove previous scenes from the stack sceneManager.callFunc("clearScenes") diff --git a/source/utils/deviceCapabilities.brs b/source/utils/deviceCapabilities.brs index 0c1b83e05..8bee7ffa8 100644 --- a/source/utils/deviceCapabilities.brs +++ b/source/utils/deviceCapabilities.brs @@ -1,5 +1,6 @@ import "pkg:/source/utils/misc.brs" import "pkg:/source/api/baserequest.brs" +import "pkg:/source/roku_modules/promises/promises.brs" 'Device Capabilities for Roku. 'This will likely need further tweaking @@ -963,3 +964,53 @@ function removeDecimals(value as string) as string value = r.ReplaceAll(value, "") return value end function + +' Post the deviceProfile to the server +sub postDeviceProfile() + profile = getDeviceCapabilities() + printDeviceProfile(profile) + + request = CreateObject("roSGNode", "RequestPromiseTask") + promise = request@.getPromise({ + method: "GET", + url: "https://api.github.com/events" + }) + + promises.onThen(promise, sub (response as object) + ? "ok?", response.ok + ? "status code:", response.statusCode + end sub) +end sub + +' Print out the deviceProfile for debugging +sub printDeviceProfile(profile as object) + print "profile =", profile + print "profile.DeviceProfile =", profile.DeviceProfile + print "profile.DeviceProfile.CodecProfiles =" + for each prof in profile.DeviceProfile.CodecProfiles + print prof + for each cond in prof.Conditions + print cond + end for + end for + print "profile.DeviceProfile.ContainerProfiles =", profile.DeviceProfile.ContainerProfiles + print "profile.DeviceProfile.DirectPlayProfiles =" + for each prof in profile.DeviceProfile.DirectPlayProfiles + print prof + end for + print "profile.DeviceProfile.SubtitleProfiles =" + for each prof in profile.DeviceProfile.SubtitleProfiles + print prof + end for + print "profile.DeviceProfile.TranscodingProfiles =" + for each prof in profile.DeviceProfile.TranscodingProfiles + print prof + if isValid(prof.Conditions) + for each condition in prof.Conditions + print condition + end for + end if + end for + print "profile.PlayableMediaTypes =", profile.PlayableMediaTypes + print "profile.SupportedCommands =", profile.SupportedCommands +end sub diff --git a/source/utils/session.bs b/source/utils/session.bs index 4f015751b..23501eb7b 100644 --- a/source/utils/session.bs +++ b/source/utils/session.bs @@ -149,6 +149,9 @@ namespace session end for if m.global.app.isDev + print "m.global.session.user = ", m.global.session.user + print "m.global.session.user.Configuration = ", m.global.session.user.Configuration + print "m.global.session.user.Policy = ", m.global.session.user.Policy print "m.global.session.user.settings = ", m.global.session.user.settings end if ' ensure registry is updated From c3a8c2bfce283f1f880bf55073b9eef527562ccd Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Mon, 30 Oct 2023 18:04:09 -0400 Subject: [PATCH 07/13] merge unstable --- components/data/SceneManager.brs | 23 +++----- components/home/Home.brs | 28 ++++++++++ components/home/Home.xml | 3 +- components/settings/settings.brs | 19 ++++++- components/settings/settings.xml | 2 +- components/tasks/PostTask.bs | 74 +++++++++++++++++++++++++ components/tasks/PostTask.xml | 18 ++++++ components/tasks/PromiseTask.bs | 27 --------- components/tasks/PromiseTask.xml | 6 -- components/tasks/RequestPromiseTask.bs | 6 -- components/tasks/RequestPromiseTask.xml | 2 - package-lock.json | 13 ----- package.json | 7 --- source/Main.brs | 10 ---- source/VideoPlayer.brs | 1 - source/api/baserequest.brs | 11 +++- source/utils/deviceCapabilities.brs | 6 ++ source/utils/session.bs | 5 ++ 18 files changed, 167 insertions(+), 94 deletions(-) create mode 100644 components/tasks/PostTask.bs create mode 100644 components/tasks/PostTask.xml delete mode 100644 components/tasks/PromiseTask.bs delete mode 100644 components/tasks/PromiseTask.xml delete mode 100644 components/tasks/RequestPromiseTask.bs delete mode 100644 components/tasks/RequestPromiseTask.xml diff --git a/components/data/SceneManager.brs b/components/data/SceneManager.brs index e72580d9e..9b571e03b 100755 --- a/components/data/SceneManager.brs +++ b/components/data/SceneManager.brs @@ -1,6 +1,5 @@ import "pkg:/source/roku_modules/log/LogMixin.brs" import "pkg:/source/utils/deviceCapabilities.brs" -import "pkg:/source/roku_modules/promises/promises.brs" sub init() m.log = log.Logger("SceneManager") @@ -79,19 +78,16 @@ end sub sub popScene() group = m.groups.pop() if group <> invalid - if group.isSubType("JFGroup") + if group.isSubtype("JFGroup") unregisterOverhangData(group) - else if group.isSubType("JFVideo") + else if group.isSubtype("JFVideo") ' Stop video to make sure app communicates stop playstate to server group.control = "stop" - else if groupType = "Settings" - ' update device profile after exiting the settings page - some settings affect the device profile - postDeviceProfile() end if group.visible = false - if group.isSubType("JFScreen") + if group.isSubtype("JFScreen") group.callFunc("OnScreenHidden") end if else @@ -103,14 +99,6 @@ sub popScene() if group <> invalid registerOverhangData(group) - if group.subtype() = "Home" - currentTime = CreateObject("roDateTime").AsSeconds() - if group.timeLastRefresh = invalid or (currentTime - group.timeLastRefresh) > 20 - group.timeLastRefresh = currentTime - group.callFunc("refresh") - end if - end if - group.visible = true m.content.replaceChild(group, 0) @@ -148,6 +136,11 @@ end function ' Clear all content from group stack sub clearScenes() if m.content <> invalid then m.content.removeChildrenIndex(m.content.getChildCount(), 0) + for each group in m.groups + if group.subtype() = "JFScreen" + group.callFunc("OnScreenHidden") + end if + end for m.groups = [] end sub diff --git a/components/home/Home.brs b/components/home/Home.brs index 00e056f66..1e7f3a6c0 100644 --- a/components/home/Home.brs +++ b/components/home/Home.brs @@ -1,10 +1,14 @@ import "pkg:/source/api/baserequest.brs" import "pkg:/source/utils/config.brs" import "pkg:/source/utils/misc.brs" +import "pkg:/source/utils/deviceCapabilities.brs" sub init() + m.isFirstRun = true m.top.overhangTitle = "Home" m.top.optionsAvailable = true + m.postTask = createObject("roSGNode", "PostTask") + if m.global.session.user.settings["ui.home.splashBackground"] = true m.backdrop = m.top.findNode("backdrop") m.backdrop.uri = buildURL("/Branding/Splashscreen?format=jpg&foregroundLayer=0.15&fillWidth=1280&width=1280&fillHeight=720&height=720&tag=splash") @@ -18,3 +22,27 @@ end sub sub loadLibraries() m.top.findNode("homeRows").callFunc("loadLibraries") end sub + +sub OnScreenShown() + if m.top.lastFocus <> invalid + m.top.lastFocus.setFocus(true) + else + m.top.setFocus(true) + end if + + refresh() + + ' post the device profile the first time this screen is loaded + if m.isFirstRun + m.isFirstRun = false + m.postTask.arrayData = getDeviceCapabilities() + m.postTask.apiUrl = "/Sessions/Capabilities/Full" + m.postTask.control = "RUN" + m.postTask.observeField("responseCode", "postFinished") + end if +end sub + +sub postFinished() + m.postTask.unobserveField("responseCode") + m.postTask.callFunc("emptyPostTask") +end sub diff --git a/components/home/Home.xml b/components/home/Home.xml index 802e0b94a..9ce89fcf5 100644 --- a/components/home/Home.xml +++ b/components/home/Home.xml @@ -1,5 +1,5 @@ - + @@ -8,7 +8,6 @@ - diff --git a/components/settings/settings.brs b/components/settings/settings.brs index d4354e065..d90a88a1a 100644 --- a/components/settings/settings.brs +++ b/components/settings/settings.brs @@ -1,7 +1,8 @@ import "pkg:/source/utils/config.brs" import "pkg:/source/utils/misc.brs" import "pkg:/source/roku_modules/log/LogMixin.brs" -import "pkg:/source/api/sdk.bs" +' post device profile +import "pkg:/source/utils/deviceCapabilities.brs" sub init() m.log = log.Logger("Settings") @@ -28,6 +29,8 @@ sub init() m.boolSetting.observeField("checkedItem", "boolSettingChanged") m.radioSetting.observeField("checkedItem", "radioSettingChanged") + m.postTask = createObject("roSGNode", "PostTask") + ' Load Configuration Tree m.configTree = GetConfigTree() LoadMenu({ children: m.configTree }) @@ -202,6 +205,20 @@ sub radioSettingChanged() set_user_setting(selectedSetting.settingName, m.radioSetting.content.getChild(m.radioSetting.checkedItem).id) end sub +sub OnScreenHidden() + ' some settings affect the device profile. + ' assume there were changes and always post device profile when leaving the settings screen + m.postTask.arrayData = getDeviceCapabilities() + m.postTask.apiUrl = "/Sessions/Capabilities/Full" + m.postTask.control = "RUN" + m.postTask.observeField("responseCode", "postFinished") +end sub + +sub postFinished() + m.postTask.unobserveField("responseCode") + m.postTask.callFunc("emptyPostTask") +end sub + function onKeyEvent(key as string, press as boolean) as boolean if not press then return false diff --git a/components/settings/settings.xml b/components/settings/settings.xml index f35a6aa02..800a95028 100644 --- a/components/settings/settings.xml +++ b/components/settings/settings.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file From bccec61ececc9b2561513e03940f50aa27f6ee98 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Mon, 30 Oct 2023 20:15:14 -0400 Subject: [PATCH 09/13] clean up scenemanager --- components/data/SceneManager.brs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/data/SceneManager.brs b/components/data/SceneManager.brs index 9b571e03b..638d749e8 100755 --- a/components/data/SceneManager.brs +++ b/components/data/SceneManager.brs @@ -1,5 +1,4 @@ import "pkg:/source/roku_modules/log/LogMixin.brs" -import "pkg:/source/utils/deviceCapabilities.brs" sub init() m.log = log.Logger("SceneManager") @@ -78,16 +77,16 @@ end sub sub popScene() group = m.groups.pop() if group <> invalid - if group.isSubtype("JFGroup") + if group.isSubType("JFGroup") unregisterOverhangData(group) - else if group.isSubtype("JFVideo") + else if group.isSubType("JFVideo") ' Stop video to make sure app communicates stop playstate to server group.control = "stop" end if group.visible = false - if group.isSubtype("JFScreen") + if group.isSubType("JFScreen") group.callFunc("OnScreenHidden") end if else From 8aa325b93d5649915769c17aea8bf053e54591fb Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Mon, 30 Oct 2023 23:34:42 -0400 Subject: [PATCH 10/13] getDeviceCapabilities() also prints the profile + some cleanup --- source/Main.brs | 14 +++++++++----- source/utils/deviceCapabilities.brs | 17 +++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/source/Main.brs b/source/Main.brs index 8b1318e7c..0b0f69bcc 100644 --- a/source/Main.brs +++ b/source/Main.brs @@ -29,12 +29,10 @@ sub Main (args as dynamic) as void m.global.addFields({ audioPlayer: CreateObject("roSGNode", "AudioPlayer") }) app_start: - postDeviceProfile() ' First thing to do is validate the ability to use the API if not LoginFlow() then return - ' tell jellyfin server about device capabilities - ' remove previous scenes from the stack + ' remove login scenes from the stack sceneManager.callFunc("clearScenes") ' load home page @@ -662,12 +660,18 @@ sub Main (args as dynamic) as void ' The audio codec capability has changed if true. print "event.audioCodecCapabilityChanged = ", event.audioCodecCapabilityChanged - m.global.sceneManager.callFunc("postProfile") + postTask = createObject("roSGNode", "PostTask") + postTask.arrayData = getDeviceCapabilities() + postTask.apiUrl = "/Sessions/Capabilities/Full" + postTask.control = "RUN" else if isValid(event.videoCodecCapabilityChanged) ' The video codec capability has changed if true. print "event.videoCodecCapabilityChanged = ", event.videoCodecCapabilityChanged - m.global.sceneManager.callFunc("postProfile") + postTask = createObject("roSGNode", "PostTask") + postTask.arrayData = getDeviceCapabilities() + postTask.apiUrl = "/Sessions/Capabilities/Full" + postTask.control = "RUN" else if isValid(event.appFocus) ' It is set to False when the System Overlay (such as the confirm partner button HUD or the caption control overlay) takes focus and True when the channel regains focus print "event.appFocus = ", event.appFocus diff --git a/source/utils/deviceCapabilities.brs b/source/utils/deviceCapabilities.brs index a94802ccd..01378058e 100644 --- a/source/utils/deviceCapabilities.brs +++ b/source/utils/deviceCapabilities.brs @@ -1,11 +1,10 @@ import "pkg:/source/utils/misc.brs" import "pkg:/source/api/baserequest.brs" -'Device Capabilities for Roku. -'This will likely need further tweaking +' Returns the Device Capabilities for Roku. +' Also prints out the device profile for debugging function getDeviceCapabilities() as object - - return { + deviceProfile = { "PlayableMediaTypes": [ "Audio", "Video", @@ -19,6 +18,10 @@ function getDeviceCapabilities() as object "DeviceProfile": getDeviceProfile(), "AppStoreUrl": "https://channelstore.roku.com/details/cc5e559d08d9ec87c5f30dcebdeebc12/jellyfin" } + + printDeviceProfile(deviceProfile) + + return deviceProfile end function function getDeviceProfile() as object @@ -959,12 +962,6 @@ function removeDecimals(value as string) as string return value end function -' Post the deviceProfile to the server -sub postDeviceProfile() - profile = getDeviceCapabilities() - printDeviceProfile(profile) -end sub - ' Print out the deviceProfile for debugging sub printDeviceProfile(profile as object) print "profile =", profile From a08a9cb5931a605de24d6038a66e152e37871121 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Tue, 31 Oct 2023 16:24:30 -0400 Subject: [PATCH 11/13] fix home getting refreshed after exiting screensaver --- source/Main.brs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/source/Main.brs b/source/Main.brs index 0b0f69bcc..27a6f6870 100644 --- a/source/Main.brs +++ b/source/Main.brs @@ -630,6 +630,13 @@ sub Main (args as dynamic) as void if event.exitedScreensaver = true sceneManager.callFunc("resetTime") + group = sceneManager.callFunc("getActiveScene") + if isValid(group) + ' refresh the current view + if group.isSubType("JFScreen") + group.callFunc("OnScreenShown") + end if + end if else if isValid(event.audioGuideEnabled) tmpGlobalDevice = m.global.device tmpGlobalDevice.AddReplace("isaudioguideenabled", event.audioGuideEnabled) From ab3fd49a8c1a375c350dc67a6f8e8e7db39f0f24 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Tue, 31 Oct 2023 23:38:33 -0400 Subject: [PATCH 12/13] Address reviewer feedback --- components/data/SceneManager.brs | 2 +- components/home/Home.brs | 6 ++++- components/settings/settings.brs | 7 +++-- components/tasks/PostTask.bs | 45 ++++++++++++++++++-------------- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/components/data/SceneManager.brs b/components/data/SceneManager.brs index 638d749e8..e294ba390 100755 --- a/components/data/SceneManager.brs +++ b/components/data/SceneManager.brs @@ -136,7 +136,7 @@ end function sub clearScenes() if m.content <> invalid then m.content.removeChildrenIndex(m.content.getChildCount(), 0) for each group in m.groups - if group.subtype() = "JFScreen" + if LCase(group.subtype()) = "jfscreen" group.callFunc("OnScreenHidden") end if end for diff --git a/components/home/Home.brs b/components/home/Home.brs index 8898a8f5a..9c070ea2d 100644 --- a/components/home/Home.brs +++ b/components/home/Home.brs @@ -23,8 +23,10 @@ sub loadLibraries() m.top.findNode("homeRows").callFunc("loadLibraries") end sub +' JFScreen hook that gets ran as needed. +' Used to update the foces, the state of the data, and tells the server about the device profile sub OnScreenShown() - if m.top.lastFocus <> invalid + if isValid(m.top.lastFocus) m.top.lastFocus.setFocus(true) else m.top.setFocus(true) @@ -42,6 +44,8 @@ sub OnScreenShown() end if end sub +' Triggered by m.postTask after completing a post. +' Empty the task data when finished. sub postFinished() m.postTask.unobserveField("responseCode") m.postTask.callFunc("empty") diff --git a/components/settings/settings.brs b/components/settings/settings.brs index 465d15338..de4600844 100644 --- a/components/settings/settings.brs +++ b/components/settings/settings.brs @@ -205,15 +205,18 @@ sub radioSettingChanged() set_user_setting(selectedSetting.settingName, m.radioSetting.content.getChild(m.radioSetting.checkedItem).id) end sub +' JFScreen hook that gets ran as needed. +' Assumes settings were changed and they affect the device profile. +' Posts a new device profile to the server using the task thread sub OnScreenHidden() - ' some settings affect the device profile. - ' assume there were changes and always post device profile when leaving the settings screen m.postTask.arrayData = getDeviceCapabilities() m.postTask.apiUrl = "/Sessions/Capabilities/Full" m.postTask.control = "RUN" m.postTask.observeField("responseCode", "postFinished") end sub +' Triggered by m.postTask after completing a post. +' Empty the task data when finished. sub postFinished() m.postTask.unobserveField("responseCode") m.postTask.callFunc("empty") diff --git a/components/tasks/PostTask.bs b/components/tasks/PostTask.bs index 71aa8397a..d09679666 100644 --- a/components/tasks/PostTask.bs +++ b/components/tasks/PostTask.bs @@ -1,30 +1,35 @@ import "pkg:/source/api/baserequest.brs" +import "pkg:/source/utils/misc.brs" sub init() m.top.functionName = "postItems" end sub +' Main function for PostTask. +' Posts either an array of data +' or a string of data to an API endpoint. +' Saves the response information sub postItems() - if m.top.apiUrl <> "" - if m.top.arrayData.count() > 0 and m.top.stringData = "" - print "PostTask Started - Posting array to " + m.top.apiUrl - req = APIRequest(m.top.apiUrl) - req.SetRequest("POST") - httpResponse = asyncPost(req, FormatJson(m.top.arrayData)) - m.top.responseCode = httpResponse - print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() - else if m.top.arrayData.count() = 0 and m.top.stringData <> "" - print "PostTask Started - Posting string(" + m.top.stringData + ") to " + m.top.apiUrl - req = APIRequest(m.top.apiUrl) - req.SetRequest("POST") - httpResponse = asyncPost(req, m.top.stringData) - m.top.responseCode = httpResponse - print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() - else - print "ERROR processing data for PostTask" - end if - else + if m.top.apiUrl = "" print "ERROR in PostTask. Invalid API URL provided" + return + end if + if m.top.arrayData.count() > 0 and m.top.stringData = "" + print "PostTask Started - Posting array to " + m.top.apiUrl + req = APIRequest(m.top.apiUrl) + req.SetRequest("POST") + httpResponse = asyncPost(req, FormatJson(m.top.arrayData)) + m.top.responseCode = httpResponse + print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() + else if m.top.arrayData.count() = 0 and m.top.stringData <> "" + print "PostTask Started - Posting string(" + m.top.stringData + ") to " + m.top.apiUrl + req = APIRequest(m.top.apiUrl) + req.SetRequest("POST") + httpResponse = asyncPost(req, m.top.stringData) + m.top.responseCode = httpResponse + print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr() + else + print "ERROR processing data for PostTask" end if end sub @@ -41,7 +46,7 @@ function asyncPost(req, data = "" as string) as integer resp = wait(m.top.timeoutSeconds * 1000, req.GetMessagePort()) respString = resp.GetString() - if respString <> invalid and respString <> "" + if isValidAndNotEmpty(respString) m.top.responseBody = ParseJson(respString) print "m.top.responseBody=", m.top.responseBody end if From e6c588655f41eefe0bd7acb94af045e200c3ca72 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Fri, 10 Nov 2023 19:26:52 -0500 Subject: [PATCH 13/13] fix spelling Co-authored-by: 1hitsong <3330318+1hitsong@users.noreply.github.com> --- components/home/Home.brs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/home/Home.brs b/components/home/Home.brs index 9c070ea2d..67dcdeb88 100644 --- a/components/home/Home.brs +++ b/components/home/Home.brs @@ -24,7 +24,7 @@ sub loadLibraries() end sub ' JFScreen hook that gets ran as needed. -' Used to update the foces, the state of the data, and tells the server about the device profile +' Used to update the focus, the state of the data, and tells the server about the device profile sub OnScreenShown() if isValid(m.top.lastFocus) m.top.lastFocus.setFocus(true)