From 57a667bd9a8674f082730c06c43b0af0ddfc4c4a Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Mon, 25 Nov 2024 11:56:52 +0100 Subject: [PATCH 1/9] Adding support for Samsung Smart Hub Preview --- config.xml | 16 ++++++ gulpfile.babel.js | 7 +++ service/service.js | 107 ++++++++++++++++++++++++++++++++++ smarthub.js | 140 +++++++++++++++++++++++++++++++++++++++++++++ tizen.js | 43 ++++++++++++++ 5 files changed, 313 insertions(+) create mode 100644 service/service.js create mode 100644 smarthub.js diff --git a/config.xml b/config.xml index 7d0a89e..c5fd29d 100644 --- a/config.xml +++ b/config.xml @@ -7,9 +7,25 @@ Jellyfin for Samsung Smart TV (Tizen). + + + + + + + jellyfin_service + Jellyfin Smarthub Preview Handler Service + + + Jellyfin + + + + + diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 8ff8079..f824434 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -107,6 +107,13 @@ function modifyIndex() { appMode.text = 'window.appMode=\'cordova\';'; injectTarget.insertBefore(appMode, apploader); + + // inject smarthub.js + const smarthub = this.createElement('script'); + smarthub.setAttribute('src', '../smarthub.js'); + smarthub.setAttribute('type', 'module'); + injectTarget.insertBefore(smarthub, apploader); + // inject tizen.js const tizen = this.createElement('script'); tizen.setAttribute('src', '../tizen.js'); diff --git a/service/service.js b/service/service.js new file mode 100644 index 0000000..063f7e8 --- /dev/null +++ b/service/service.js @@ -0,0 +1,107 @@ +var pkg_id = 'AprZAARz4r' +var app_id = 'AprZAARz4r.Jellyfin' +var remote_message_port = undefined; +var local_message_port = undefined; +//var message_port_listener = undefined; +var watchId = undefined; + +function onReceived(data, remoteMsgPort) { + + /*remote_message_port = tizen.messageport.requestRemoteMessagePort(app_id, "AprZAARz4r.DataChannel"); + const dataa = [{key: "KEY", value: "Valasz"}]; + remote_message_port.sendMessage(dataa);*/ + //console.log('onReceived : ' + JSON.stringify(data) + ' remotePort : ' + remoteMsgPort); + sendMessage("KEY", "Recieved!!!!!!") + console.log("service start"); +}; + +function sendMessage(key, value) { + if (remote_message_port === undefined) { + remote_message_port = tizen.messageport.requestRemoteMessagePort(app_id, "AprZAARz4r.DataChannel"); + } + remote_message_port.sendMessage([{ key, value }]); + console.error("Message port is undefined"); + +} + +module.exports.onStart = function () { + //console.log("onStart is called"); + //pkg_id = tizen.application.getCurrentApplication().appInfo.packageId; + //app_id = tizen.application.getCurrentApplication().appInfo.id; + + try { + if (remote_message_port === undefined) { + remote_message_port = tizen.messageport.requestRemoteMessagePort(app_id, "AprZAARz4r.DataChannel"); + } + sendMessage("KEY", "Service Started with id:" + pkg_id) + + try { + local_message_port = tizen.messageport.requestLocalMessagePort("CHANNEL2"); + watchId = local_message_port.addMessagePortListener(onReceived); + sendMessage("KEY", "WatchID :" + watchId) + } + catch (e) { + sendMessage("KEY", "Error " + e.message) + //const data4 = [{ key: "KEY", value: "Error" + e.message }]; + //remote_message_port.sendMessage(data4); + } + + } + catch (e) { + sendMessage("KEY", "Error " + e.message) + } + +}; + +module.exports.onRequest = function () { + try { + console.log('Request Callback'); + var reqAppControl = tizen.application.getCurrentApplication().getRequestedAppControl(); + if (!!reqAppControl) { + + if (reqAppControl.appControl.data[0].key == 'Preview') { + var previewData = reqAppControl.appControl.data[0].value; + var previewData2 = JSON.parse(previewData); + sendMessage("KEY", "Preview Data recieved:" + previewData) + + try { + webapis.preview.setPreviewData(JSON.stringify(previewData2), + function () { + console.log('setPreviewData SuccessCallback'); + // please terminate service after setting preview data + sendMessage("KEY", "Preview Set!") + tizen.application.getCurrentApplication().exit(); + }, + function (e) { + console.log('PreviewData Setting failed : ' + e.message); + sendMessage("KEY", 'PreviewData Setting failed : ' + e.message) + } + ); + } + catch (e) { + sendMessage("KEY", 'PreviewData Setting exception : ' + e.message) + } + } + local_message_port = tizen.messageport.requestLocalMessagePort("CHANNEL2"); + watchId = local_message_port.addMessagePortListener(onReceived); + } + else { + sendMessage("KEY", 'Unknown Request ') + } + } + catch (e) { + sendMessage("KEY", 'On error exception : ' + e.message) + } +} + + +module.exports.onStop = function () { + console.log("onStop is called"); + sendMessage("KEY", 'on Stop: : ') +}; + + +module.exports.onExit = function () { + console.log("onExit is callback"); + sendMessage("KEY", 'Exit Callback') +} \ No newline at end of file diff --git a/smarthub.js b/smarthub.js new file mode 100644 index 0000000..bfc9b4d --- /dev/null +++ b/smarthub.js @@ -0,0 +1,140 @@ +/*( + + function () { + 'use strict'; +*/ +import { Service } from 'wrt:service'; + +var pkg_id = tizen.application.getCurrentApplication().appInfo.packageId; +var service_id = pkg_id + ".service"; +var localJsonData = undefined; + + +function create_title_json(title_data) { + + var action_data = + { + serverid: title_data.ServerId, + id: title_data.Id + }; + + + var obj = { + title: "S"+title_data.ParentIndexNumber+":E"+title_data.IndexNumber+" - "+title_data.Name, + subtitle: "Subtitle", + image_ratio: "1by1", + image_url: "https://artworks.thetvdb.com/banners/fanart/original/78901-80.jpg", + //image_url: "http://192.168.31.62:8096/Items/5f6a38a4adfd66d4d3e838f0bb19e050/Images/Thumb?fillHeight=122&fillWidth=216&format=jpg&quality=96&tag=3e557bd01b366b69311d41a555d2bd96", + action_data: JSON.stringify(action_data), // Escaped JSON string + is_playable: true + }; + return obj +} + +function create_smart_view_json(data) { + var obj = { + sections: [ + { + title: "Ajanlott", + tiles: [create_title_json(data[0])] + }, + { + title: "Folytasd", + tiles: [ + create_title_json(data[1]), + create_title_json(data[2]), + create_title_json(data[3]), + create_title_json(data[4]), + ] + } + ] + }; + return obj +} + +function send_message_to_service() { + + try { + tizen.application.launchAppControl( + new tizen.ApplicationControl( + 'http://tizen.org/appcontrol/operation/pick', + null, + 'image/jpeg', + null, + [ + new tizen.ApplicationControlData('caller', [JSON.stringify(localJsonData)]), + new tizen.ApplicationControlData('Preview', [JSON.stringify(localJsonData)]) + ] + ), + service_id, + () => console.log('Message: ' + JSON.stringify(localJsonData) + ' sent to ' + service_id), + (error) => console.error('Launch failed:', error.message) + ); + } catch (error) { + console.error("Error sending message:", error); + + } +} + + +var OnReceived = function (ui_data) { + console.log("Received Data in Service : " + ui_data[0].value); +}; + +function delay(time) { + return new Promise(resolve => setTimeout(resolve, time)); +} + + +var local_message_port = tizen.messageport.requestLocalMessagePort(pkg_id + ".DataChannel"); +var message_port_listener = local_message_port.addMessagePortListener(OnReceived); + + +var interval = setInterval(function () { + // get elem + if (typeof ApiClient == 'undefined') return; + clearInterval(interval); + const address = ApiClient.getResumableItems(ApiClient.getCurrentUserId()) + .then((response) => { + return response.Items; + }); + + const printAddress = async () => { + const a = await address; + + console.log(JSON.stringify(create_smart_view_json(a))); + localJsonData = create_smart_view_json(a); + + delay(2000).then(() => { + console.log('ran after 2 second1 passed'); + send_message_to_service(localJsonData); + }); + + /*delay(6000).then(() => { + console.log('ran after 6 second1 passed'); + send_message_to_service(localJsonData); + });*/ + }; + + + var my_service = new Service(service_id); + my_service.start().then(function () { + console.log("Succeeded to start service333"); + }, + function (error) { + console.log("Failed to start service: " + error); + + }); + + printAddress(); + + + + + // the rest of the code +}, 1000); + +/* + } +)(); +*/ \ No newline at end of file diff --git a/tizen.js b/tizen.js index 51c85dd..3be6af0 100644 --- a/tizen.js +++ b/tizen.js @@ -199,4 +199,47 @@ } window.addEventListener('viewshow', updateKeys); + + //Deeplink + var text = ''; + + function deepLink() { + var requestedAppControl = tizen.application.getCurrentApplication().getRequestedAppControl(); + var appControlData; + var actionData; + + if (requestedAppControl) { + appControlData = requestedAppControl.appControl.data; // get appcontrol data. action_data is in it. + text = '[TestApp] appControlData : ' + JSON.stringify(appControlData); + console.log(text); + + for (var i = 0; i < appControlData.length; i++) { + console.log(appControlData[i].key); + if (appControlData[i].key == 'PAYLOAD') { // find PAYLOAD property. + console.log('Payload'); + actionData = JSON.parse(appControlData[i].value[0]).values; // Get action_data + console.log('aaaa ' + actionData); + if (JSON.parse(actionData).serverid) { // in case Tile is video. + var serverid = JSON.parse(actionData).serverid + var id = JSON.parse(actionData).id + + text = '[TestApp] videoIdx : ' + serverid; + var newUrl = "file:///www/index.html#/details?id=" + id + "&serverId=" + serverid; + window.location.href = newUrl; + console.log(newUrl); + + } + } + } + } else { + console.log('no req app control'); + } + } + + + // add appcontrol event with deepLink function + window.addEventListener('appcontrol', deepLink); + // call deepLink function for first load + deepLink(); + })(); From e3b40ad1c81306851c7d5f5c1d4e959478b7d703 Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Wed, 4 Dec 2024 20:21:21 +0100 Subject: [PATCH 2/9] Working version wit tizen 4.x,5.x --- gulpfile.babel.js | 2 +- service/service.js | 169 ++++++++++++----------- smarthub.js | 325 ++++++++++++++++++++++++++++----------------- tizen.js | 72 +++++++--- 4 files changed, 353 insertions(+), 215 deletions(-) diff --git a/gulpfile.babel.js b/gulpfile.babel.js index f824434..f8761b0 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -111,7 +111,7 @@ function modifyIndex() { // inject smarthub.js const smarthub = this.createElement('script'); smarthub.setAttribute('src', '../smarthub.js'); - smarthub.setAttribute('type', 'module'); + smarthub.setAttribute('defer', ''); injectTarget.insertBefore(smarthub, apploader); // inject tizen.js diff --git a/service/service.js b/service/service.js index 063f7e8..40195b8 100644 --- a/service/service.js +++ b/service/service.js @@ -1,107 +1,116 @@ -var pkg_id = 'AprZAARz4r' -var app_id = 'AprZAARz4r.Jellyfin' -var remote_message_port = undefined; -var local_message_port = undefined; -//var message_port_listener = undefined; +var packageId = tizen.application.getCurrentApplication().appInfo.packageId; +var applicationId = packageId +'.Jellyfin' +var remoteMessagePort = undefined; +var localMessagePort = undefined; var watchId = undefined; - +var isServiceStarted = false; + +/** + * Handles incoming messages from the local message port. + * + * @param {Array} data - Array of key-value pairs received via the message port. + * @param {Object} remoteMsgPort - The remote message port instance that sent the message. + */ function onReceived(data, remoteMsgPort) { - /*remote_message_port = tizen.messageport.requestRemoteMessagePort(app_id, "AprZAARz4r.DataChannel"); - const dataa = [{key: "KEY", value: "Valasz"}]; - remote_message_port.sendMessage(dataa);*/ - //console.log('onReceived : ' + JSON.stringify(data) + ' remotePort : ' + remoteMsgPort); - sendMessage("KEY", "Recieved!!!!!!") - console.log("service start"); + sendMessage("Key0 " + data[0].key) + + if (data[0].key == "Preview") { + + var previewData = data[0].value; + sendMessage("Preview Data recieved:" + previewData) + var previewData2 = JSON.parse(previewData); + + try { + webapis.preview.setPreviewData(JSON.stringify(previewData2), + function () { + console.log('Preview Set!'); + sendMessage("Preview Set!") + // please terminate service after setting preview data + tizen.application.getCurrentApplication().exit(); + }, + function (e) { + console.log('PreviewData Setting failed : ' + e.message); + sendMessage('PreviewData Setting failed : ' + e.message) + } + ); + } + catch (e) { + sendMessage('PreviewData Setting exception : ' + e.message) + console.log('PreviewData Setting failed : ' + e.message); + } + + + } }; -function sendMessage(key, value) { - if (remote_message_port === undefined) { - remote_message_port = tizen.messageport.requestRemoteMessagePort(app_id, "AprZAARz4r.DataChannel"); +/** + * Sends a message to the remote message port. + * + * @param {string} value - The value to send in the message. + * @param {string} [key="KEY"] - The key associated with the value. Defaults to "KEY". + */ +function sendMessage(value, key) { + key = key || "KEY"; + if (remoteMessagePort === undefined) { + remoteMessagePort = tizen.messageport.requestRemoteMessagePort(applicationId, "DATACHANNEL"); + } + if (remoteMessagePort ) { + try { + remoteMessagePort .sendMessage([{ key, value }]); + } catch (e) { + console.error("Error sending message:", e.message); + } + } else { + console.log("Message port is undefined"); } - remote_message_port.sendMessage([{ key, value }]); - console.error("Message port is undefined"); } -module.exports.onStart = function () { - //console.log("onStart is called"); - //pkg_id = tizen.application.getCurrentApplication().appInfo.packageId; - //app_id = tizen.application.getCurrentApplication().appInfo.id; +/** + * Starts the service by setting up a local message port and listener. + */ +function start() { + if (isServiceStarted) { + console.log("Service already started."); + return; + } + try { + sendMessage("Service Started:" + packageId ) - try { - if (remote_message_port === undefined) { - remote_message_port = tizen.messageport.requestRemoteMessagePort(app_id, "AprZAARz4r.DataChannel"); - } - sendMessage("KEY", "Service Started with id:" + pkg_id) + localMessagePort = tizen.messageport.requestLocalMessagePort("DATACHANNEL"); + watchId = localMessagePort.addMessagePortListener(onReceived); + + /* For debugging purposes, somehow it is not working on the emulator. */ + sendMessage("WatchID :" + watchId) + isServiceStarted = true; + console.log("Service started successfully."); - try { - local_message_port = tizen.messageport.requestLocalMessagePort("CHANNEL2"); - watchId = local_message_port.addMessagePortListener(onReceived); - sendMessage("KEY", "WatchID :" + watchId) } catch (e) { - sendMessage("KEY", "Error " + e.message) - //const data4 = [{ key: "KEY", value: "Error" + e.message }]; - //remote_message_port.sendMessage(data4); + sendMessage("Creating of local port not sucessfull: " + e.message) + console.log("Creating of local port not sucessfull: " + e.message); } + +} - } - catch (e) { - sendMessage("KEY", "Error " + e.message) - } - +module.exports.onStart = function () { + start(); }; + module.exports.onRequest = function () { - try { - console.log('Request Callback'); - var reqAppControl = tizen.application.getCurrentApplication().getRequestedAppControl(); - if (!!reqAppControl) { - - if (reqAppControl.appControl.data[0].key == 'Preview') { - var previewData = reqAppControl.appControl.data[0].value; - var previewData2 = JSON.parse(previewData); - sendMessage("KEY", "Preview Data recieved:" + previewData) - - try { - webapis.preview.setPreviewData(JSON.stringify(previewData2), - function () { - console.log('setPreviewData SuccessCallback'); - // please terminate service after setting preview data - sendMessage("KEY", "Preview Set!") - tizen.application.getCurrentApplication().exit(); - }, - function (e) { - console.log('PreviewData Setting failed : ' + e.message); - sendMessage("KEY", 'PreviewData Setting failed : ' + e.message) - } - ); - } - catch (e) { - sendMessage("KEY", 'PreviewData Setting exception : ' + e.message) - } - } - local_message_port = tizen.messageport.requestLocalMessagePort("CHANNEL2"); - watchId = local_message_port.addMessagePortListener(onReceived); - } - else { - sendMessage("KEY", 'Unknown Request ') - } - } - catch (e) { - sendMessage("KEY", 'On error exception : ' + e.message) - } + start(); } module.exports.onStop = function () { - console.log("onStop is called"); - sendMessage("KEY", 'on Stop: : ') + console.log("Service stopping..."); + sendMessage("Service stopping..."); }; module.exports.onExit = function () { - console.log("onExit is callback"); - sendMessage("KEY", 'Exit Callback') + console.log("Service exiting..."); + sendMessage("Service exiting..."); } \ No newline at end of file diff --git a/smarthub.js b/smarthub.js index bfc9b4d..2e5a9d7 100644 --- a/smarthub.js +++ b/smarthub.js @@ -1,140 +1,227 @@ -/*( - - function () { - 'use strict'; -*/ -import { Service } from 'wrt:service'; - -var pkg_id = tizen.application.getCurrentApplication().appInfo.packageId; -var service_id = pkg_id + ".service"; -var localJsonData = undefined; - - -function create_title_json(title_data) { - - var action_data = - { - serverid: title_data.ServerId, - id: title_data.Id - }; - +(function () { + + var packageId = tizen.application.getCurrentApplication().appInfo.packageId; + var serviceId = packageId + ".service"; + var smartViewJsonData = undefined; + var remoteMessagePort = undefined; + +/** + * Creates a JSON object representing one title for the smart view. + * + * @param {Object} title_data - The title data containing details about the media items. + * @param {string} title_data.ServerId - The server ID associated with the media. + * @param {string} title_data.Id - The unique ID of the media item. + * @param {number} title_data.ParentIndexNumber - The parent index number of the media. + * @param {number} title_data.IndexNumber - The index number of the media. + * @param {string} title_data.Name - The name of the media item. + * @param {string} title_data.SeriesName - The series name of the media item. + * @param {string} title_data.ParentBackdropItemId - The ID for the backdrop image. + * @param {Object} title_data.UserData - User-specific data, including played percentage. + * @param {number} title_data.UserData.PlayedPercentage - Percentage of the media played. + * @returns {Object|null} The formatted title JSON object or `null` if data is invalid. + */ + function generateTitleJson(title_data) { + + if (!title_data) { + console.warn("Missing title_data"); + return null; + } + + var action_data = + { + serverid: title_data.ServerId, + id: title_data.Id + }; + var title = null; + + var playedPercentage = ""; + + if (title_data.UserData && title_data.UserData.PlayedPercentage) { + playedPercentage = "&percentPlayed=" + title_data.UserData.PlayedPercentage; + } + + if(title_data.Type =="Episode"){ + title = { + title: "S" + title_data.ParentIndexNumber + ":E" + title_data.IndexNumber + " - " + title_data.Name, + subtitle: title_data.SeriesName, + image_ratio: "16by9", + image_url: ApiClient.serverAddress() + "/Items/" + title_data.ParentBackdropItemId + "/Images/Backdrop?format=jpg&quality=96&fillHeight=250" + playedPercentage, + action_data: JSON.stringify(action_data), + is_playable: true + };} + else if(title_data.Type =="Movie"){ + title = { + title: title_data.Name, + image_ratio: "16by9", + image_url: ApiClient.serverAddress() +"/Items/" + title_data.Id + "/Images/Thumb?format=jpg&quality=96&fillHeight=250" + playedPercentage, + action_data: JSON.stringify(action_data), + is_playable: true + }; + } + return title; + } - var obj = { - title: "S"+title_data.ParentIndexNumber+":E"+title_data.IndexNumber+" - "+title_data.Name, - subtitle: "Subtitle", - image_ratio: "1by1", - image_url: "https://artworks.thetvdb.com/banners/fanart/original/78901-80.jpg", - //image_url: "http://192.168.31.62:8096/Items/5f6a38a4adfd66d4d3e838f0bb19e050/Images/Thumb?fillHeight=122&fillWidth=216&format=jpg&quality=96&tag=3e557bd01b366b69311d41a555d2bd96", - action_data: JSON.stringify(action_data), // Escaped JSON string - is_playable: true - }; - return obj -} -function create_smart_view_json(data) { - var obj = { - sections: [ - { - title: "Ajanlott", - tiles: [create_title_json(data[0])] - }, - { - title: "Folytasd", - tiles: [ - create_title_json(data[1]), - create_title_json(data[2]), - create_title_json(data[3]), - create_title_json(data[4]), - ] +/** + * Creates a JSON object for the smart view containing multiple sections and their tiles. + * + * @param {Array} sectionsData - Array of objects representing each section's metadata and content. + * @param {string} sectionsData[].section_title - Title of the section (e.g., "Next Up", "Continue Watching"). + * @param {number} sectionsData[].limit - Maximum number of items to include in the section. + * @param {Array} sectionsData[].data - Array of media items to populate the section's tiles. + * + * @returns {Object} A JSON object with `sections` for the smart view. + * Each section contains a title and an array of tiles. + * If no valid sections are provided, the returned object will have an empty `sections` array. + */ + function generateSmartViewJson(sectionsData) { + + + // Validate input data + if (!Array.isArray(sectionsData) || sectionsData.length === 0) { + console.warn("Invalid or empty sections data."); + return { sections: [] }; + } + + // Initialize the smart view JSON object + var smart_view_json = { sections: [] }; + + // Populate Sections + sectionsData.forEach(section => { + if (Array.isArray(section.data) && section.data.length > 0) { + const tiles = section.data.slice(0, section.limit).map(generateTitleJson).filter(Boolean); + if (tiles.length > 0) { + smart_view_json.sections.push({ + title: section.section_title, + tiles: tiles + }); + } } - ] - }; - return obj -} - -function send_message_to_service() { - - try { - tizen.application.launchAppControl( - new tizen.ApplicationControl( - 'http://tizen.org/appcontrol/operation/pick', - null, - 'image/jpeg', - null, - [ - new tizen.ApplicationControlData('caller', [JSON.stringify(localJsonData)]), - new tizen.ApplicationControlData('Preview', [JSON.stringify(localJsonData)]) - ] - ), - service_id, - () => console.log('Message: ' + JSON.stringify(localJsonData) + ' sent to ' + service_id), - (error) => console.error('Launch failed:', error.message) - ); - } catch (error) { - console.error("Error sending message:", error); + }); + return smart_view_json; + } + + + /** + * Launches the Tizen application control to start a service + * + * @throws {Error} Logs any error encountered during the service launch process. + */ + function startService() { + try { + tizen.application.launchAppControl( + new tizen.ApplicationControl( + 'http://tizen.org/appcontrol/operation/pick', + null, + 'image/jpeg', + null, + [ + new tizen.ApplicationControlData('caller', ["JSON.stringify(smartViewJsonData)"]), + new tizen.ApplicationControlData('Preview', ["JSON.stringify(smartViewJsonData)"]) + ] + ), + serviceId, + () => console.log('Message sent to ' + serviceId), + (error) => console.error('Launch failed:', error.message) + ); + } catch (error) { + console.error("Error sending message:", error); + } } -} - -var OnReceived = function (ui_data) { - console.log("Received Data in Service : " + ui_data[0].value); -}; - -function delay(time) { - return new Promise(resolve => setTimeout(resolve, time)); -} - - -var local_message_port = tizen.messageport.requestLocalMessagePort(pkg_id + ".DataChannel"); -var message_port_listener = local_message_port.addMessagePortListener(OnReceived); + /** + * Sends a key-value message to the Tizen service using the message port. + * + * @param {string} key - The key for the message. + * @param {string} value - The value for the message. + */ + function sendMessageToService(key, value) { + if (remoteMessagePort === undefined) { + remoteMessagePort = tizen.messageport.requestRemoteMessagePort(serviceId, "DATACHANNEL"); + } + + if (remoteMessagePort) { + remoteMessagePort.sendMessage([{ key, value }]); + } + else { + console.error("Message port is undefined"); + } + } -var interval = setInterval(function () { - // get elem - if (typeof ApiClient == 'undefined') return; - clearInterval(interval); - const address = ApiClient.getResumableItems(ApiClient.getCurrentUserId()) - .then((response) => { - return response.Items; - }); - const printAddress = async () => { - const a = await address; + /** + * Sends a smart view update request to the service. + * + * @param {Object} smartViewJsonData - The smart view data to send. + */ + function sendSmartViewUpdateRequest(smartViewJsonData) { + sendMessageToService('Preview', JSON.stringify(smartViewJsonData)); - console.log(JSON.stringify(create_smart_view_json(a))); - localJsonData = create_smart_view_json(a); + } - delay(2000).then(() => { - console.log('ran after 2 second1 passed'); - send_message_to_service(localJsonData); - }); - /*delay(6000).then(() => { - console.log('ran after 6 second1 passed'); - send_message_to_service(localJsonData); - });*/ + /** + * Callback function for receiving messages from the Tizen service. + * + * @param {Array} ui_data - The received data array from the service. + */ + var OnReceived = function (ui_data) { + console.log("Received Data in Service : " + ui_data[0].value); + if (ui_data[0].value == 'Service exiting...') + window.scriptReady = true; }; - var my_service = new Service(service_id); - my_service.start().then(function () { - console.log("Succeeded to start service333"); - }, - function (error) { - console.log("Failed to start service: " + error); - - }); - - printAddress(); + /** + * Delays execution for a specified time. + * + * @param {number} time - Time in milliseconds to delay. + * @returns {Promise} A promise that resolves after the specified delay. + */ + function delay(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } + var localMessagePort = tizen.messageport.requestLocalMessagePort("DATACHANNEL"); + localMessagePort.addMessagePortListener(OnReceived); + + + var interval = setInterval(function () { + + if (typeof ApiClient == 'undefined') return; + clearInterval(interval); + const fetchData = async () => { + try { + const [resumableItems, nextUpEpisodes] = await Promise.all([ + ApiClient.getResumableItems(ApiClient.getCurrentUserId()), + ApiClient.getNextUpEpisodes(ApiClient.getCurrentUserId()) + ]); + + // Generate smart view data + smartViewJsonData = generateSmartViewJson([ + { section_title: "Next Up", limit: 2, data: nextUpEpisodes.Items }, + { section_title: "Continue Watching", limit: 4, data: resumableItems.Items } + ]); + console.log("Generated SmartViewResult: \n" + JSON.stringify(smartViewJsonData)); + + // Delay and send the smart view update request + await delay(2000); + sendSmartViewUpdateRequest(smartViewJsonData); + } catch (error) { + console.error("Error fetching data: ", error); + window.scriptReady = true; + } + }; + + // Start the service and fetch the data + startService(); + fetchData(); + }, 1000); - // the rest of the code -}, 1000); -/* - } +} )(); -*/ \ No newline at end of file diff --git a/tizen.js b/tizen.js index 3be6af0..d628e15 100644 --- a/tizen.js +++ b/tizen.js @@ -80,6 +80,32 @@ console.log.apply(console, arguments); } + function loadScript(src) { + return new Promise((resolve, reject) => { + var script = document.createElement('script'); + script.src = src; + script.type = 'text/javascript'; + window.scriptReady = false; + + script.onload = function () { + const checkReady = () => { + if (window.scriptReady) { + resolve(); + } else { + setTimeout(checkReady, 100); + } + }; + checkReady(); + }; + + script.onerror = function () { + reject(new Error(`SmartHubPreview not loaded ${src}`)); + }; + + document.head.appendChild(script); + }); + } + window.NativeShell = { AppHost: { init: function () { @@ -109,9 +135,15 @@ return AppInfo.deviceName; }, - exit: function () { - postMessage('AppHost.exit'); - tizen.application.getCurrentApplication().exit(); + exit: async function () { + try { + console.log('Refresh SmartHubPrewiev on exit...'); + await loadScript('../smarthub.js'); + postMessage('AppHost.exit'); + tizen.application.getCurrentApplication().exit(); + } catch (error) { + console.error('Error:', error.message); + } }, getDefaultLayout: function () { @@ -200,30 +232,40 @@ window.addEventListener('viewshow', updateKeys); - //Deeplink + /** + * Handles deep linking by processing requested application control data + * to retrieve payload information and redirect to a specific URL. + * * + * @see https://developer.samsung.com/smarttv/develop/guides/smart-hub-preview/implementing-public-preview.html#implementing-public-preview-deep-links + */ var text = ''; - function deepLink() { + + // Retrieve the app control request for the current application var requestedAppControl = tizen.application.getCurrentApplication().getRequestedAppControl(); - var appControlData; - var actionData; + var appControlData; // Stores app control data + var actionData; // Stores parsed action data if (requestedAppControl) { + // Retrieve app control data appControlData = requestedAppControl.appControl.data; // get appcontrol data. action_data is in it. - text = '[TestApp] appControlData : ' + JSON.stringify(appControlData); + text = 'appControlData : ' + JSON.stringify(appControlData); console.log(text); + // Iterate over app control data to find the PAYLOAD key for (var i = 0; i < appControlData.length; i++) { - console.log(appControlData[i].key); - if (appControlData[i].key == 'PAYLOAD') { // find PAYLOAD property. - console.log('Payload'); - actionData = JSON.parse(appControlData[i].value[0]).values; // Get action_data - console.log('aaaa ' + actionData); - if (JSON.parse(actionData).serverid) { // in case Tile is video. + if (appControlData[i].key == 'PAYLOAD') { + + // Parse the PAYLOAD value to extract action data + actionData = JSON.parse(appControlData[i].value[0]).values; + console.log('Get element info ' + actionData); + + // If the action data contains a server ID, assume it is a valid Jellifyn link + if (JSON.parse(actionData).serverid) { var serverid = JSON.parse(actionData).serverid var id = JSON.parse(actionData).id - text = '[TestApp] videoIdx : ' + serverid; + // Construct the URL for the details page and redirect var newUrl = "file:///www/index.html#/details?id=" + id + "&serverId=" + serverid; window.location.href = newUrl; console.log(newUrl); From 82f60c66acd89de1359270c80ccf84ef56096ef0 Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Thu, 12 Dec 2024 13:41:08 +0100 Subject: [PATCH 3/9] Working with all emulator --- service/service.js | 65 +++++++++++++++++++++++++++++----- smarthub.js | 88 +++++++++++++++++++++++++++++++++++++++------- tizen.js | 6 ++-- 3 files changed, 136 insertions(+), 23 deletions(-) diff --git a/service/service.js b/service/service.js index 40195b8..38f8992 100644 --- a/service/service.js +++ b/service/service.js @@ -1,6 +1,6 @@ -var packageId = tizen.application.getCurrentApplication().appInfo.packageId; -var applicationId = packageId +'.Jellyfin' -var remoteMessagePort = undefined; +var packageId = tizen.application.getCurrentApplication().appInfo.packageId; +var applicationId = packageId + '.Jellyfin' +var remoteMessagePort = undefined; var localMessagePort = undefined; var watchId = undefined; var isServiceStarted = false; @@ -11,6 +11,7 @@ var isServiceStarted = false; * @param {Array} data - Array of key-value pairs received via the message port. * @param {Object} remoteMsgPort - The remote message port instance that sent the message. */ + function onReceived(data, remoteMsgPort) { sendMessage("Key0 " + data[0].key) @@ -42,7 +43,8 @@ function onReceived(data, remoteMsgPort) { } -}; +}; + /** * Sends a message to the remote message port. @@ -53,7 +55,7 @@ function onReceived(data, remoteMsgPort) { function sendMessage(value, key) { key = key || "KEY"; if (remoteMessagePort === undefined) { - remoteMessagePort = tizen.messageport.requestRemoteMessagePort(applicationId, "DATACHANNEL"); + remoteMessagePort = tizen.messageport.requestRemoteMessagePort(applicationId, packageId); } if (remoteMessagePort ) { try { @@ -73,18 +75,20 @@ function sendMessage(value, key) { function start() { if (isServiceStarted) { console.log("Service already started."); + sendMessage("Service already started.") return; } try { sendMessage("Service Started:" + packageId ) - localMessagePort = tizen.messageport.requestLocalMessagePort("DATACHANNEL"); + localMessagePort = tizen.messageport.requestLocalMessagePort(packageId); watchId = localMessagePort.addMessagePortListener(onReceived); /* For debugging purposes, somehow it is not working on the emulator. */ sendMessage("WatchID :" + watchId) isServiceStarted = true; console.log("Service started successfully."); + sendMessage("Service started successfully."); } catch (e) { @@ -94,13 +98,58 @@ function start() { } + +function handleDataInRequest() +{ + try { + + var reqAppControl = tizen.application.getCurrentApplication().getRequestedAppControl(); + if (!!reqAppControl) { + + if (reqAppControl.appControl.data[0].key == 'Preview') { + var previewData = reqAppControl.appControl.data[0].value; + var previewData2 = JSON.parse(previewData); + sendMessage("Preview Data recieved:" + previewData) + + try { + webapis.preview.setPreviewData(JSON.stringify(previewData2), + function () { + console.log('setPreviewData SuccessCallback'); + // please terminate service after setting preview data + sendMessage("Preview Set!") + tizen.application.getCurrentApplication().exit(); + }, + function (e) { + console.log('PreviewData Setting failed : ' + e.message); + sendMessage('PreviewData Setting failed : ' + e.message) + } + ); + } + catch (e) { + sendMessage('PreviewData Setting exception : ' + e.message) + } + } + + } + else + { + sendMessage('Unknown Request ') + } + } + catch (e) { + sendMessage('On error exception : ' + e.message) + } +} + module.exports.onStart = function () { start(); + sendMessage("OnStart recieved") }; - module.exports.onRequest = function () { - start(); + start(); + sendMessage("onRequest recieved"); + handleDataInRequest(); } diff --git a/smarthub.js b/smarthub.js index 2e5a9d7..956bdeb 100644 --- a/smarthub.js +++ b/smarthub.js @@ -1,9 +1,74 @@ -(function () { +/*(function () {*/ var packageId = tizen.application.getCurrentApplication().appInfo.packageId; var serviceId = packageId + ".service"; var smartViewJsonData = undefined; var remoteMessagePort = undefined; + var localMessagePort = undefined; + var messagePortListener = undefined; + + var localJsonData = { + "sections": [ + { + "title": "Next Up", + "tiles": [ + { + "title": "S11:E2 - 2. epizód", + "subtitle": "Hívják a bábát", + "image_ratio": "16by9", + "image_url": "http://192.168.31.62:8096/Items/5f6a38a4adfd66d4d3e838f0bb19e050/Images/Backdrop?format=jpg&quality=96&fillHeight=250", + "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"65073997942266eccaf61c0e745c9f38\"}", + "is_playable": true + }, + { + "title": "S8:E22 - The Proposal", + "subtitle": "The Goldbergs (2013)", + "image_ratio": "16by9", + "image_url": "http://192.168.31.62:8096/Items/263cf89eb8bbfd0cf58f66daa421af38/Images/Backdrop?format=jpg&quality=96&fillHeight=250&percentPlayed=10.541836545589325", + "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"328b67745026c24c82bbeaf4ce569b9b\"}", + "is_playable": true + } + ] + }, + { + "title": "Continue Watching", + "tiles": [ + { + "title": "Men in Black - Sötét zsaruk 2.", + "image_ratio": "16by9", + "image_url": "http://192.168.31.62:8096/Items/7e149af90e93f9d3036529486862100f/Images/Thumb?format=jpg&quality=96&fillHeight=250&percentPlayed=58.40761698414322", + "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"7e149af90e93f9d3036529486862100f\"}", + "is_playable": true + }, + { + "title": "S2:E10 - 10. epizód", + "subtitle": "Psych - Dilis detektívek", + "image_ratio": "16by9", + "image_url": "http://192.168.31.62:8096/Items/019cf831e3a7cdf5bfaa3c968b3b5e93/Images/Backdrop?format=jpg&quality=96&fillHeight=250&percentPlayed=21.214575024153394", + "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"9bf364266aa530d637284d680a26516e\"}", + "is_playable": true + }, + { + "title": "S4:E6 - Recipe for Death II: Kiss the Cook", + "subtitle": "The Goldbergs (2013)", + "image_ratio": "16by9", + "image_url": "http://192.168.31.62:8096/Items/263cf89eb8bbfd0cf58f66daa421af38/Images/Backdrop?format=jpg&quality=96&fillHeight=250&percentPlayed=17.31943792713447", + "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"05d927035a6dcf525770dd46dcfaa21f\"}", + "is_playable": true + }, + { + "title": "S8:E22 - The Proposal", + "subtitle": "The Goldbergs (2013)", + "image_ratio": "16by9", + "image_url": "http://192.168.31.62:8096/Items/263cf89eb8bbfd0cf58f66daa421af38/Images/Backdrop?format=jpg&quality=96&fillHeight=250&percentPlayed=10.541836545589325", + "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"328b67745026c24c82bbeaf4ce569b9b\"}", + "is_playable": true + } + ] + } + ] +}; + /** * Creates a JSON object representing one title for the smart view. @@ -108,7 +173,10 @@ * * @throws {Error} Logs any error encountered during the service launch process. */ - function startService() { + function startService(smartViewJsonData) { + console.log('Starting Service'), + localMessagePort = tizen.messageport.requestLocalMessagePort(packageId); + messagePortListener = localMessagePort.addMessagePortListener(OnReceived); try { tizen.application.launchAppControl( new tizen.ApplicationControl( @@ -117,8 +185,7 @@ 'image/jpeg', null, [ - new tizen.ApplicationControlData('caller', ["JSON.stringify(smartViewJsonData)"]), - new tizen.ApplicationControlData('Preview', ["JSON.stringify(smartViewJsonData)"]) + new tizen.ApplicationControlData('Preview', [JSON.stringify(smartViewJsonData)]) ] ), serviceId, @@ -139,7 +206,7 @@ */ function sendMessageToService(key, value) { if (remoteMessagePort === undefined) { - remoteMessagePort = tizen.messageport.requestRemoteMessagePort(serviceId, "DATACHANNEL"); + remoteMessagePort = tizen.messageport.requestRemoteMessagePort(serviceId, packageId); } if (remoteMessagePort) { @@ -185,8 +252,6 @@ } - var localMessagePort = tizen.messageport.requestLocalMessagePort("DATACHANNEL"); - localMessagePort.addMessagePortListener(OnReceived); var interval = setInterval(function () { @@ -206,10 +271,10 @@ { section_title: "Continue Watching", limit: 4, data: resumableItems.Items } ]); console.log("Generated SmartViewResult: \n" + JSON.stringify(smartViewJsonData)); - // Delay and send the smart view update request await delay(2000); - sendSmartViewUpdateRequest(smartViewJsonData); + startService(smartViewJsonData); + //sendSmartViewUpdateRequest(smartViewJsonData); } catch (error) { console.error("Error fetching data: ", error); window.scriptReady = true; @@ -217,11 +282,10 @@ }; // Start the service and fetch the data - startService(); fetchData(); }, 1000); - +/* } -)(); +)();*/ diff --git a/tizen.js b/tizen.js index d628e15..dc237d3 100644 --- a/tizen.js +++ b/tizen.js @@ -137,9 +137,9 @@ exit: async function () { try { - console.log('Refresh SmartHubPrewiev on exit...'); - await loadScript('../smarthub.js'); - postMessage('AppHost.exit'); + //console.log('Refresh SmartHubPrewiev on exit...'); + //await loadScript('../smarthub.js'); + //postMessage('AppHost.exit'); tizen.application.getCurrentApplication().exit(); } catch (error) { console.error('Error:', error.message); From 072e211899dd813385d7f4f4c399a69b445eb0fd Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Fri, 13 Dec 2024 20:17:02 +0100 Subject: [PATCH 4/9] Refactor code and drop unnecesarry functions --- service/service.js | 154 ++++++++++++--------------------------- smarthub.js | 175 ++++++++++++++------------------------------- tizen.js | 31 +------- 3 files changed, 101 insertions(+), 259 deletions(-) diff --git a/service/service.js b/service/service.js index 38f8992..770196e 100644 --- a/service/service.js +++ b/service/service.js @@ -1,50 +1,19 @@ var packageId = tizen.application.getCurrentApplication().appInfo.packageId; var applicationId = packageId + '.Jellyfin' var remoteMessagePort = undefined; -var localMessagePort = undefined; -var watchId = undefined; -var isServiceStarted = false; + /** - * Handles incoming messages from the local message port. + * Sends a message to Application and write out the log. + * It is needed because logs are not visible from service * - * @param {Array} data - Array of key-value pairs received via the message port. - * @param {Object} remoteMsgPort - The remote message port instance that sent the message. + * @param {string} value - The value to send in the message and wite to console */ - -function onReceived(data, remoteMsgPort) { - - sendMessage("Key0 " + data[0].key) - - if (data[0].key == "Preview") { - - var previewData = data[0].value; - sendMessage("Preview Data recieved:" + previewData) - var previewData2 = JSON.parse(previewData); - - try { - webapis.preview.setPreviewData(JSON.stringify(previewData2), - function () { - console.log('Preview Set!'); - sendMessage("Preview Set!") - // please terminate service after setting preview data - tizen.application.getCurrentApplication().exit(); - }, - function (e) { - console.log('PreviewData Setting failed : ' + e.message); - sendMessage('PreviewData Setting failed : ' + e.message) - } - ); - } - catch (e) { - sendMessage('PreviewData Setting exception : ' + e.message) - console.log('PreviewData Setting failed : ' + e.message); - } - - - } -}; - +function logAndSend(value) +{ + console.log(value); + sendMessage(value); +} /** * Sends a message to the remote message port. @@ -59,7 +28,7 @@ function sendMessage(value, key) { } if (remoteMessagePort ) { try { - remoteMessagePort .sendMessage([{ key, value }]); + remoteMessagePort.sendMessage([{ key, value }]); } catch (e) { console.error("Error sending message:", e.message); } @@ -69,97 +38,64 @@ function sendMessage(value, key) { } -/** - * Starts the service by setting up a local message port and listener. - */ -function start() { - if (isServiceStarted) { - console.log("Service already started."); - sendMessage("Service already started.") - return; - } - try { - sendMessage("Service Started:" + packageId ) - - localMessagePort = tizen.messageport.requestLocalMessagePort(packageId); - watchId = localMessagePort.addMessagePortListener(onReceived); - - /* For debugging purposes, somehow it is not working on the emulator. */ - sendMessage("WatchID :" + watchId) - isServiceStarted = true; - console.log("Service started successfully."); - sendMessage("Service started successfully."); - - } - catch (e) { - sendMessage("Creating of local port not sucessfull: " + e.message) - console.log("Creating of local port not sucessfull: " + e.message); - } - -} - - function handleDataInRequest() { try { - var reqAppControl = tizen.application.getCurrentApplication().getRequestedAppControl(); - if (!!reqAppControl) { - - if (reqAppControl.appControl.data[0].key == 'Preview') { - var previewData = reqAppControl.appControl.data[0].value; - var previewData2 = JSON.parse(previewData); - sendMessage("Preview Data recieved:" + previewData) - try { - webapis.preview.setPreviewData(JSON.stringify(previewData2), - function () { - console.log('setPreviewData SuccessCallback'); - // please terminate service after setting preview data - sendMessage("Preview Set!") - tizen.application.getCurrentApplication().exit(); - }, - function (e) { - console.log('PreviewData Setting failed : ' + e.message); - sendMessage('PreviewData Setting failed : ' + e.message) - } - ); - } - catch (e) { - sendMessage('PreviewData Setting exception : ' + e.message) + if (!!reqAppControl) { + var appControlData = reqAppControl.appControl.data; + + // Iterate through all keys in appControl.data + for (var i = 0; i < appControlData.length; i++) { + var key = appControlData[i].key; + var value = appControlData[i].value; + + if (key === 'Preview') { + var previewData = value; + var previewData2 = JSON.parse(previewData); + logAndSend("Preview Data received: " + previewData); + + try { + webapis.preview.setPreviewData( + JSON.stringify(previewData2), + function () { + logAndSend("Preview Set!"); + tizen.application.getCurrentApplication().exit(); + }, + function (e) { + logAndSend("PreviewData Setting failed: " + e.message); + } + ); + } catch (e) { + logAndSend("PreviewData Setting exception: " + e.message); + } + } else { + logAndSend("Unhandled key: " + key + ", value: " + value); } } - } - else - { - sendMessage('Unknown Request ') - } } catch (e) { - sendMessage('On error exception : ' + e.message) + logAndSend('On error exception : ' + e.message); } } module.exports.onStart = function () { - start(); - sendMessage("OnStart recieved") + logAndSend('OnStart recieved'); + handleDataInRequest(); }; module.exports.onRequest = function () { - start(); - sendMessage("onRequest recieved"); - handleDataInRequest(); + logAndSend('onRequest recieved'); } module.exports.onStop = function () { - console.log("Service stopping..."); - sendMessage("Service stopping..."); + logAndSend('Service stopping...'); }; module.exports.onExit = function () { - console.log("Service exiting..."); - sendMessage("Service exiting..."); + logAndSend("Service exiting..."); } \ No newline at end of file diff --git a/smarthub.js b/smarthub.js index 956bdeb..2572401 100644 --- a/smarthub.js +++ b/smarthub.js @@ -1,4 +1,3 @@ -/*(function () {*/ var packageId = tizen.application.getCurrentApplication().appInfo.packageId; var serviceId = packageId + ".service"; @@ -7,79 +6,17 @@ var localMessagePort = undefined; var messagePortListener = undefined; - var localJsonData = { - "sections": [ - { - "title": "Next Up", - "tiles": [ - { - "title": "S11:E2 - 2. epizód", - "subtitle": "Hívják a bábát", - "image_ratio": "16by9", - "image_url": "http://192.168.31.62:8096/Items/5f6a38a4adfd66d4d3e838f0bb19e050/Images/Backdrop?format=jpg&quality=96&fillHeight=250", - "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"65073997942266eccaf61c0e745c9f38\"}", - "is_playable": true - }, - { - "title": "S8:E22 - The Proposal", - "subtitle": "The Goldbergs (2013)", - "image_ratio": "16by9", - "image_url": "http://192.168.31.62:8096/Items/263cf89eb8bbfd0cf58f66daa421af38/Images/Backdrop?format=jpg&quality=96&fillHeight=250&percentPlayed=10.541836545589325", - "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"328b67745026c24c82bbeaf4ce569b9b\"}", - "is_playable": true - } - ] - }, - { - "title": "Continue Watching", - "tiles": [ - { - "title": "Men in Black - Sötét zsaruk 2.", - "image_ratio": "16by9", - "image_url": "http://192.168.31.62:8096/Items/7e149af90e93f9d3036529486862100f/Images/Thumb?format=jpg&quality=96&fillHeight=250&percentPlayed=58.40761698414322", - "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"7e149af90e93f9d3036529486862100f\"}", - "is_playable": true - }, - { - "title": "S2:E10 - 10. epizód", - "subtitle": "Psych - Dilis detektívek", - "image_ratio": "16by9", - "image_url": "http://192.168.31.62:8096/Items/019cf831e3a7cdf5bfaa3c968b3b5e93/Images/Backdrop?format=jpg&quality=96&fillHeight=250&percentPlayed=21.214575024153394", - "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"9bf364266aa530d637284d680a26516e\"}", - "is_playable": true - }, - { - "title": "S4:E6 - Recipe for Death II: Kiss the Cook", - "subtitle": "The Goldbergs (2013)", - "image_ratio": "16by9", - "image_url": "http://192.168.31.62:8096/Items/263cf89eb8bbfd0cf58f66daa421af38/Images/Backdrop?format=jpg&quality=96&fillHeight=250&percentPlayed=17.31943792713447", - "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"05d927035a6dcf525770dd46dcfaa21f\"}", - "is_playable": true - }, - { - "title": "S8:E22 - The Proposal", - "subtitle": "The Goldbergs (2013)", - "image_ratio": "16by9", - "image_url": "http://192.168.31.62:8096/Items/263cf89eb8bbfd0cf58f66daa421af38/Images/Backdrop?format=jpg&quality=96&fillHeight=250&percentPlayed=10.541836545589325", - "action_data": "{\"serverid\":\"4513d1afddd14932aa9cbddc0756cc87\",\"id\":\"328b67745026c24c82bbeaf4ce569b9b\"}", - "is_playable": true - } - ] - } - ] -}; - - + /** * Creates a JSON object representing one title for the smart view. * * @param {Object} title_data - The title data containing details about the media items. * @param {string} title_data.ServerId - The server ID associated with the media. - * @param {string} title_data.Id - The unique ID of the media item. - * @param {number} title_data.ParentIndexNumber - The parent index number of the media. + * @param {string} title_data.Id - The unique ID of the "Episode" + * @param {number} title_data.ParentIndexNumber - The "Series" index number of the media. * @param {number} title_data.IndexNumber - The index number of the media. - * @param {string} title_data.Name - The name of the media item. - * @param {string} title_data.SeriesName - The series name of the media item. + * @param {string} title_data.Name - The name of the Movie + * @param {string} title_data.SeriesName - The name of the Series * @param {string} title_data.ParentBackdropItemId - The ID for the backdrop image. * @param {Object} title_data.UserData - User-specific data, including played percentage. * @param {number} title_data.UserData.PlayedPercentage - Percentage of the media played. @@ -169,11 +106,12 @@ /** - * Launches the Tizen application control to start a service + * Launches the Tizen application and send Smartview data * + * @param {Object} smartViewJsonData - The title data containing details about the media items. * @throws {Error} Logs any error encountered during the service launch process. */ - function startService(smartViewJsonData) { + function startServiceAndUpdateSmartView(smartViewJsonData) { console.log('Starting Service'), localMessagePort = tizen.messageport.requestLocalMessagePort(packageId); messagePortListener = localMessagePort.addMessagePortListener(OnReceived); @@ -198,48 +136,32 @@ } - /** - * Sends a key-value message to the Tizen service using the message port. - * - * @param {string} key - The key for the message. - * @param {string} value - The value for the message. - */ - function sendMessageToService(key, value) { - if (remoteMessagePort === undefined) { - remoteMessagePort = tizen.messageport.requestRemoteMessagePort(serviceId, packageId); - } - - if (remoteMessagePort) { - remoteMessagePort.sendMessage([{ key, value }]); - } - else { - console.error("Message port is undefined"); - } - } - - - /** - * Sends a smart view update request to the service. - * - * @param {Object} smartViewJsonData - The smart view data to send. - */ - function sendSmartViewUpdateRequest(smartViewJsonData) { - sendMessageToService('Preview', JSON.stringify(smartViewJsonData)); - - } - - /** * Callback function for receiving messages from the Tizen service. * * @param {Array} ui_data - The received data array from the service. */ var OnReceived = function (ui_data) { - console.log("Received Data in Service : " + ui_data[0].value); - if (ui_data[0].value == 'Service exiting...') - window.scriptReady = true; + console.log("Received Data from Service : " + ui_data[0].value); + if (ui_data[0].value == 'Service stopping...'){ + window.smartHubUpdated = true; + localMessagePort.removeMessagePortListener(messagePortListener); + + } }; + const waitForSmartHubUpdate = () => { + return new Promise((resolve) => { + const interval = setInterval(() => { + if (window.smartHubUpdated === true) { + clearInterval(interval); + resolve(); + } + }, 100); + }); +}; + + /** * Delays execution for a specified time. @@ -251,14 +173,15 @@ return new Promise(resolve => setTimeout(resolve, time)); } - - - - var interval = setInterval(function () { + (async function runSmartViewUpdater() { - if (typeof ApiClient == 'undefined') return; - clearInterval(interval); - const fetchData = async () => { + while (typeof ApiClient === 'undefined') { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + + const runSmartViewUpdate = async () => { + window.smartHubUpdated = false; try { const [resumableItems, nextUpEpisodes] = await Promise.all([ ApiClient.getResumableItems(ApiClient.getCurrentUserId()), @@ -266,26 +189,36 @@ ]); // Generate smart view data - smartViewJsonData = generateSmartViewJson([ + const smartViewJsonData = generateSmartViewJson([ { section_title: "Next Up", limit: 2, data: nextUpEpisodes.Items }, { section_title: "Continue Watching", limit: 4, data: resumableItems.Items } ]); + console.log("Generated SmartViewResult: \n" + JSON.stringify(smartViewJsonData)); + // Delay and send the smart view update request await delay(2000); - startService(smartViewJsonData); - //sendSmartViewUpdateRequest(smartViewJsonData); + startServiceAndUpdateSmartView(smartViewJsonData); + await waitForSmartHubUpdate(); + } catch (error) { console.error("Error fetching data: ", error); - window.scriptReady = true; + window.smartHubUpdated = true; } }; - // Start the service and fetch the data - fetchData(); + window.runSmartViewUpdate = runSmartViewUpdate; + + // Refresh SmartView every 10min. + while (true) { + const startTime = Date.now(); + + await runSmartViewUpdate(); + + const elapsedTime = Date.now() - startTime; + const remainingTime = Math.max(600000 - elapsedTime, 0); + await new Promise(resolve => setTimeout(resolve, remainingTime)); + } + })(); - }, 1000); -/* -} -)();*/ diff --git a/tizen.js b/tizen.js index dc237d3..8bd1212 100644 --- a/tizen.js +++ b/tizen.js @@ -80,32 +80,6 @@ console.log.apply(console, arguments); } - function loadScript(src) { - return new Promise((resolve, reject) => { - var script = document.createElement('script'); - script.src = src; - script.type = 'text/javascript'; - window.scriptReady = false; - - script.onload = function () { - const checkReady = () => { - if (window.scriptReady) { - resolve(); - } else { - setTimeout(checkReady, 100); - } - }; - checkReady(); - }; - - script.onerror = function () { - reject(new Error(`SmartHubPreview not loaded ${src}`)); - }; - - document.head.appendChild(script); - }); - } - window.NativeShell = { AppHost: { init: function () { @@ -137,12 +111,11 @@ exit: async function () { try { - //console.log('Refresh SmartHubPrewiev on exit...'); - //await loadScript('../smarthub.js'); - //postMessage('AppHost.exit'); + await runSmartViewUpdate(); tizen.application.getCurrentApplication().exit(); } catch (error) { console.error('Error:', error.message); + tizen.application.getCurrentApplication().exit(); } }, From 7dd30ae2b97d6990b2a29297ec7c22b79d93885a Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Fri, 13 Dec 2024 23:18:15 +0100 Subject: [PATCH 5/9] Add support of Deep-link Return Key Policy ( https://developer.samsung.com/smarttv/develop/guides/smart-hub-preview/smart-hub-preview.html#Deep-link-Return-Key-Policy) From a detail page within an application, clicking the "Return/Exit" key must display the previous page in the application. https://developer.samsung.com/media/2424/uxguidelines4.png --- smarthub.js | 6 ++++++ tizen.js | 31 +++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/smarthub.js b/smarthub.js index 2572401..4c015a7 100644 --- a/smarthub.js +++ b/smarthub.js @@ -43,6 +43,11 @@ } if(title_data.Type =="Episode"){ + + action_data.type = 'episode'; + action_data.seasonid = title_data.SeasonId; + action_data.seriesid = title_data.SeriesId; + title = { title: "S" + title_data.ParentIndexNumber + ":E" + title_data.IndexNumber + " - " + title_data.Name, subtitle: title_data.SeriesName, @@ -52,6 +57,7 @@ is_playable: true };} else if(title_data.Type =="Movie"){ + action_data.type = 'movie'; title = { title: title_data.Name, image_ratio: "16by9", diff --git a/tizen.js b/tizen.js index 8bd1212..111f56f 100644 --- a/tizen.js +++ b/tizen.js @@ -235,13 +235,32 @@ // If the action data contains a server ID, assume it is a valid Jellifyn link if (JSON.parse(actionData).serverid) { - var serverid = JSON.parse(actionData).serverid - var id = JSON.parse(actionData).id - + var serverid = JSON.parse(actionData).serverid; + var id = JSON.parse(actionData).id; + var type = JSON.parse(actionData).type; + var seasonid = JSON.parse(actionData).seasonid; + var seriesid = JSON.parse(actionData).seriesid; + + + /* Based on Deep-link Return Key Policy ( https://developer.samsung.com/smarttv/develop/guides/smart-hub-preview/smart-hub-preview.html#Deep-link-Return-Key-Policy) + From a detail page within an application, clicking the “Return/Exit” key must display the previous page in the application. + https://developer.samsung.com/media/2424/uxguidelines4.png + */ + history.pushState({}, '', 'file:///www/home.html'); + if (type =='episode') + { + history.pushState({}, '', 'file:///www/index.html#/tv.html'); + history.pushState({}, '', "file:///www/index.html#/details?id=" + seriesid + "&serverId=" + serverid); + history.pushState({}, '', "file:///www/index.html#/details?id=" + seasonid + "&serverId=" + serverid); + + } + if (type =='movie') + { + history.pushState({}, '', 'file:///www/index.html#/movies.html'); + } // Construct the URL for the details page and redirect - var newUrl = "file:///www/index.html#/details?id=" + id + "&serverId=" + serverid; - window.location.href = newUrl; - console.log(newUrl); + history.pushState({}, '', "file:///www/index.html#/details?id=" + id + "&serverId=" + serverid); + } } From 4b1ee26b3e51d471d15cbb2f107555c8b4d2d5ea Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Fri, 13 Dec 2024 23:47:38 +0100 Subject: [PATCH 6/9] Make compatibility with tizen 4.0 --- service/service.js | 2 +- smarthub.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/service/service.js b/service/service.js index 770196e..59bfb29 100644 --- a/service/service.js +++ b/service/service.js @@ -83,11 +83,11 @@ function handleDataInRequest() module.exports.onStart = function () { logAndSend('OnStart recieved'); - handleDataInRequest(); }; module.exports.onRequest = function () { logAndSend('onRequest recieved'); + handleDataInRequest(); } diff --git a/smarthub.js b/smarthub.js index 4c015a7..9a2b407 100644 --- a/smarthub.js +++ b/smarthub.js @@ -149,7 +149,7 @@ */ var OnReceived = function (ui_data) { console.log("Received Data from Service : " + ui_data[0].value); - if (ui_data[0].value == 'Service stopping...'){ + if (ui_data[0].value == 'Service stopping...' || ui_data[0].value == 'Service exiting...'){ window.smartHubUpdated = true; localMessagePort.removeMessagePortListener(messagePortListener); From d2d2995b3c77d75fc3a48766f9498eecdc41dbfa Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Mon, 16 Dec 2024 20:38:52 +0100 Subject: [PATCH 7/9] Make image URL generation better --- smarthub.js | 138 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 7 deletions(-) diff --git a/smarthub.js b/smarthub.js index 9a2b407..3f5bdb3 100644 --- a/smarthub.js +++ b/smarthub.js @@ -6,6 +6,129 @@ var localMessagePort = undefined; var messagePortListener = undefined; + + + + +/** Get the URL of the card's image. + * @param {Object} item - Item for which to generate tileimageurk + * @returns {CardImageUrl} Object representing the URL of the card's image. + */ +function getTileImageUrl(item) { + item = item.ProgramInfo || item; + + options ={ + preferThumb: true, + inheritThumb: true, + } + + preferThumb = true; + let height = 250; + let imgUrl = null; + let imgTag = null; + let imgType = null; + let itemId = null; + + /* eslint-disable sonarjs/no-duplicated-branches */ + if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) { + imgType = 'Thumb'; + imgTag = item.ImageTags.Thumb; + } else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) { + imgType = 'Thumb'; + imgTag = item.SeriesThumbImageTag; + itemId = item.SeriesId; + } else if (options.preferThumb && item.ParentThumbItemId && options.inheritThumb !== false && item.MediaType !== 'Photo') { + imgType = 'Thumb'; + imgTag = item.ParentThumbImageTag; + itemId = item.ParentThumbItemId; + } else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) { + imgType = 'Backdrop'; + imgTag = item.BackdropImageTags[0]; + forceName = true; + } else if (options.preferThumb && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false && item.Type === 'Episode') { + imgType = 'Backdrop'; + imgTag = item.ParentBackdropImageTags[0]; + itemId = item.ParentBackdropItemId; + } else if (item.ImageTags && item.ImageTags.Primary && (item.Type !== 'Episode' || item.ChildCount !== 0)) { + imgType = 'Primary'; + imgTag = item.ImageTags.Primary; + + if (primaryImageAspectRatio && uiAspect) { + coverImage = (Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect) <= 0.2; + } + } else if (item.SeriesPrimaryImageTag) { + imgType = 'Primary'; + imgTag = item.SeriesPrimaryImageTag; + itemId = item.SeriesId; + } else if (item.PrimaryImageTag) { + imgType = 'Primary'; + imgTag = item.PrimaryImageTag; + itemId = item.PrimaryImageItemId; + + if (primaryImageAspectRatio && uiAspect) { + coverImage = (Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect) <= 0.2; + } + } else if (item.ParentPrimaryImageTag) { + imgType = 'Primary'; + imgTag = item.ParentPrimaryImageTag; + itemId = item.ParentPrimaryImageItemId; + } else if (item.AlbumId && item.AlbumPrimaryImageTag) { + imgType = 'Primary'; + imgTag = item.AlbumPrimaryImageTag; + itemId = item.AlbumId; + + if (primaryImageAspectRatio && uiAspect) { + coverImage = (Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect) <= 0.2; + } + } else if (item.Type === 'Season' && item.ImageTags && item.ImageTags.Thumb) { + imgType = 'Thumb'; + imgTag = item.ImageTags.Thumb; + } else if (item.BackdropImageTags && item.BackdropImageTags.length) { + imgType = 'Backdrop'; + imgTag = item.BackdropImageTags[0]; + } else if (item.ImageTags && item.ImageTags.Thumb) { + imgType = 'Thumb'; + imgTag = item.ImageTags.Thumb; + } else if (item.SeriesThumbImageTag && options.inheritThumb !== false) { + imgType = 'Thumb'; + imgTag = item.SeriesThumbImageTag; + itemId = item.SeriesId; + } else if (item.ParentThumbItemId && options.inheritThumb !== false) { + imgType = 'Thumb'; + imgTag = item.ParentThumbImageTag; + itemId = item.ParentThumbItemId; + } else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) { + imgType = 'Backdrop'; + imgTag = item.ParentBackdropImageTags[0]; + itemId = item.ParentBackdropItemId; + } + /* eslint-enable sonarjs/no-duplicated-branches */ + + if (!itemId) { + itemId = item.Id; + } + + if (imgTag && imgType) { + var params = { + type: imgType, + fillHeight: height, + quality: 96, + tag: imgTag, + format: "jpg" + }; + var playedPercentage = item && item.UserData && item.UserData.PlayedPercentage; + if (playedPercentage !== null && playedPercentage !== undefined) { + params.percentPlayed = playedPercentage; + } + imgUrl = ApiClient.getScaledImageUrl(itemId, params); + } + + return imgUrl; + +} + + + /** * Creates a JSON object representing one title for the smart view. @@ -29,6 +152,7 @@ return null; } + var action_data = { serverid: title_data.ServerId, @@ -36,23 +160,23 @@ }; var title = null; - var playedPercentage = ""; + var imgURL = getTileImageUrl(title_data); - if (title_data.UserData && title_data.UserData.PlayedPercentage) { - playedPercentage = "&percentPlayed=" + title_data.UserData.PlayedPercentage; - } if(title_data.Type =="Episode"){ action_data.type = 'episode'; action_data.seasonid = title_data.SeasonId; action_data.seriesid = title_data.SeriesId; + series_episode = ""; + if (title_data.ParentIndexNumber !== undefined && title_data.IndexNumber !== undefined) + series_episode = "S" + title_data.ParentIndexNumber + ":E" + title_data.IndexNumber + " - " title = { - title: "S" + title_data.ParentIndexNumber + ":E" + title_data.IndexNumber + " - " + title_data.Name, + title: series_episode + title_data.Name, subtitle: title_data.SeriesName, image_ratio: "16by9", - image_url: ApiClient.serverAddress() + "/Items/" + title_data.ParentBackdropItemId + "/Images/Backdrop?format=jpg&quality=96&fillHeight=250" + playedPercentage, + image_url: imgURL, action_data: JSON.stringify(action_data), is_playable: true };} @@ -61,7 +185,7 @@ title = { title: title_data.Name, image_ratio: "16by9", - image_url: ApiClient.serverAddress() +"/Items/" + title_data.Id + "/Images/Thumb?format=jpg&quality=96&fillHeight=250" + playedPercentage, + image_url: imgURL, action_data: JSON.stringify(action_data), is_playable: true }; From 9b77f9faf82b5ef2cbfd7b568c55310f5e3fa35a Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Mon, 16 Dec 2024 21:29:00 +0100 Subject: [PATCH 8/9] Add options to get less items to make run faster --- smarthub.js | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/smarthub.js b/smarthub.js index 3f5bdb3..417cb4d 100644 --- a/smarthub.js +++ b/smarthub.js @@ -313,24 +313,37 @@ function getTileImageUrl(item) { const runSmartViewUpdate = async () => { window.smartHubUpdated = false; try { + var nextUpLimit = 2 + var resumeLimit = 4 + + const baseOptions = { + Recursive: true, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Thumb', + EnableTotalRecordCount: false, + }; + + var resumableOptions = Object.assign({}, baseOptions, { Limit: resumeLimit }); + var nextUpOptions = Object.assign({}, baseOptions, { Limit: nextUpLimit, UserId: ApiClient.getCurrentUserId() }); + const [resumableItems, nextUpEpisodes] = await Promise.all([ - ApiClient.getResumableItems(ApiClient.getCurrentUserId()), - ApiClient.getNextUpEpisodes(ApiClient.getCurrentUserId()) + ApiClient.getResumableItems(ApiClient.getCurrentUserId(), resumableOptions), + ApiClient.getNextUpEpisodes(nextUpOptions) ]); - + // Generate smart view data const smartViewJsonData = generateSmartViewJson([ - { section_title: "Next Up", limit: 2, data: nextUpEpisodes.Items }, - { section_title: "Continue Watching", limit: 4, data: resumableItems.Items } + { section_title: "Next Up", limit: nextUpLimit, data: nextUpEpisodes.Items }, + { section_title: "Continue Watching", limit: resumeLimit, data: resumableItems.Items } ]); - + console.log("Generated SmartViewResult: \n" + JSON.stringify(smartViewJsonData)); - + // Delay and send the smart view update request await delay(2000); startServiceAndUpdateSmartView(smartViewJsonData); await waitForSmartHubUpdate(); - + } catch (error) { console.error("Error fetching data: ", error); window.smartHubUpdated = true; From 858e5d5333f1185ffb1eef5cdc584f93a0041800 Mon Sep 17 00:00:00 2001 From: Morpheus133 Date: Tue, 17 Dec 2024 09:44:10 +0100 Subject: [PATCH 9/9] Remove unused privileges --- config.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config.xml b/config.xml index c5fd29d..f1a2095 100644 --- a/config.xml +++ b/config.xml @@ -21,10 +21,6 @@ Jellyfin - - - -