/// declare empty global vars // control var var room = ""; // control var var type = ""; // the search term var searchTerm = ""; // item to power the TV (from androidtv binding) var powerSwitchItem = ""; // open app on TV (from androiddebugbridge binding) var openAppStringItem = ""; // close app on TV (from androiddebugbridge binding) var closeAppStringItem = ""; // read current app on TV (from androiddebugbridge binding) var currentAppStringItem = ""; // play item by id on Jellyfin client (from jellyfin binding) var playByIdStringItem = ""; // play item by name on Jellyfin client (from jellyfin binding) var playByNameStringItem = ""; // send android keycode (from androiddebugbridge binding, also available at androidtv binding) var androidKeyCode = ""; // Jellifin client thing id (from jellyfin binding) var jellyfinThingId = ""; // Jellifin client package name on android device var jellyfinPackageName = ""; // Audio sink id var audioSinkId = ""; // my items const SEARCH_ITEMS = { BEDROOM: "play_bedroom", BEDROOM_MOVIE: "play_movie_bedroom", LIVING: "play_livingroom", LIVING_MOVIE: "play_movie_livingroom", } const ROOM_NAMES = { [SEARCH_ITEMS.BEDROOM]: "habitación", [SEARCH_ITEMS.BEDROOM_MOVIE]: "habitación", [SEARCH_ITEMS.LIVING]: "salón", [SEARCH_ITEMS.LIVING_MOVIE]: "salón", } /// setup var { osgi } = require("openhab"); var voiceManager = osgi.getService("org.openhab.core.voice.VoiceManager"); var dialogContext = voiceManager.getLastDialogContext(); if (dialogContext) { audioSinkId = dialogContext.sink().getId(); } else { console.warning("missing dialog context"); } if (typeof event != "undefined") { items.getItem(event.itemName).postUpdate(""); room = event.itemName; if (event.itemName == SEARCH_ITEMS.BEDROOM_MOVIE || event.itemName == SEARCH_ITEMS.LIVING_MOVIE) { type = "movie" } searchTerm = event.itemCommand.toString(); } else { room = SEARCH_ITEMS.BEDROOM; searchTerm = ""; } // try { initControlVars(room); } catch (e) { speak(e.message); } // main function (async function (searchTerm, roomName, type) { const movies = loadJSON("automation/js/movies.json"); const tvShows = loadJSON("automation/js/tv_shows.json"); let bestItem; switch (type) { case "tv_show": bestItem = getBestItem(tvShows, searchTerm).item; break; case "movie": bestItem = getBestItem(movies, searchTerm).item; break; default: bestItem = getBestItem([...tvShows, ...movies], searchTerm).item; break; } speak(`reproduciendo ${bestItem.NameES ? bestItem.NameES : bestItem.Name} en ${roomName}`); await openJellyfin(); items.getItem(playByIdStringItem).sendCommand(bestItem.Id); })(searchTerm, ROOM_NAMES[room], type) .then(() => console.log("done")) .catch(error => console.error(error.toString())); // Utils function initControlVars(room) { switch (room) { case SEARCH_ITEMS.LIVING: case SEARCH_ITEMS.LIVING_MOVIE: powerSwitchItem = "TelevisionSalon_Power"; openAppStringItem = "TelevisionSalon_Android_StartPackage"; closeAppStringItem = "TelevisionSalonFireTV_StopPackage"; currentAppStringItem = "TelevisionSalonFireStick_CurrentPackage"; playByIdStringItem = "TelevisionSalonJellyfin_PlayById"; playByNameStringItem = "TelevisionSalonJellyfin_PlayByName"; androidKeyCode = "TelevisionSalon_Android_SendKeyEvent"; jellyfinThingId = "jellyfin:client:12f4fdc4c0:e6d031b552c53e7829e12e24f63cdd64f39a82f2"; jellyfinPackageName = "org.jellyfin.androidtv"; break; case SEARCH_ITEMS.BEDROOM: case SEARCH_ITEMS.BEDROOM_MOVIE: powerSwitchItem = "Projector_Power"; openAppStringItem = "Projector_StartPackage"; closeAppStringItem = "Projector_StopPackage"; currentAppStringItem = "Projector_CurrentPackage"; playByIdStringItem = "ProjectorJellyfin_PlayById"; playByNameStringItem = "ProjectorJellyfin_PlayByName"; androidKeyCode = "Projector_SendKeyEvent"; jellyfinThingId = "jellyfin:client:f0ab727a55764ff48233efebd9c10fb9:126c7f211eaabe7cd8a6d20fd48b24c0413793d5"; jellyfinPackageName = "org.jellyfin.androidtv"; break; default: throw new Error("Habitación desconocida"); } } async function openJellyfin() { const { REFRESH } = require("@runtime"); // turn on tv if (powerSwitchItem.length && items.getItem(powerSwitchItem).state.toString() != "ON") { items.getItem(powerSwitchItem).sendCommand("ON"); await waitSeconds(6); } // force exit screensaver items.getItem(androidKeyCode).sendCommand("KEYCODE_WAKEUP"); // ensure jellyfin app is open and online on openHAB const currentApp = items.getItem(currentAppStringItem).state.toString(); var jellyfinThingStatus = things.getThing(jellyfinThingId).status.toString(); if (currentApp == jellyfinPackageName && jellyfinThingStatus == "ONLINE") { console.log("Jellyfin is already launched"); } else { items.getItem(androidKeyCode).sendCommand("KEYCODE_REFRESH"); items.getItem(closeAppStringItem).sendCommand(jellyfinPackageName); await waitSeconds(1); items.getItem(openAppStringItem).sendCommand(jellyfinPackageName); await waitSeconds(5); items.getItem(playByNameStringItem).sendCommand(REFRESH); let retryCounter = 0; let jellyfinClientStatus = things.getThing(jellyfinThingId).status.toString(); while (retryCounter < 15 && jellyfinClientStatus != "ONLINE") { items.getItem(playByNameStringItem).sendCommand(REFRESH); await waitSeconds(3); retryCounter++; jellyfinClientStatus = things.getThing(jellyfinThingId).status.toString(); console.log( "Waiting for Jellyfin client to be online, try ", retryCounter, ": ", jellyfinClientStatus ); } } } function waitSeconds(seconds) { return new Promise(resolve => setTimeout(resolve, seconds * 1000)); } function loadJSON(path) { const { Files, Paths } = require("@runtime"); const json_data = Files.static.readString(Paths.static.get(path)); return JSON.parse(json_data); } function speak(text) { console.info("Speaking by " + audioSinkId + ": " + text); actions.Voice.say(text, null, audioSinkId); } function getBestItem(items, searchTerm) { return items .map((item) => ({ item, score: scoreNames(searchTerm, (item.NameES ? item.NameES : item.Name).toLowerCase()) })) .sort((a, b) => b.score - a.score)[0]; } function scoreNames(search, itemName) { const searchTermNumber = search.split(' ').length; const itemNameParts = itemName.split(' '); let score = 0; const specialCharsRegex = /[^\w\sáéíóúïüñç]/g; if(itemNameParts.length > searchTermNumber) { for (let i = 0; i < itemNameParts.length - searchTermNumber + 1; i++) { const itemNameSegment = itemNameParts .slice(i, i + searchTermNumber) .join(" ") .replace(specialCharsRegex, ''); const coeff = dicesCoefficient(searchTerm, itemNameSegment); if (coeff > score) { score = coeff; } } } const coeff = dicesCoefficient(searchTerm, itemName.replace(specialCharsRegex, '')); if (coeff > score) { score = coeff; } return score; } // Dice's Coefficient implementation https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Dice%27s_coefficient#Javascript. function dicesCoefficient(first, second) { first = first.replace(/\s+/g, ''); second = second.replace(/\s+/g, ''); if (first === second) return 1; // identical or empty if (first.length < 2 || second.length < 2) return 0; // if either is a 0-letter or 1-letter string let firstBigrams = new Map(); for (let i = 0; i < first.length - 1; i++) { const bigram = first.substring(i, i + 2); const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1; firstBigrams.set(bigram, count); }; let intersectionSize = 0; for (let i = 0; i < second.length - 1; i++) { const bigram = second.substring(i, i + 2); const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0; if (count > 0) { firstBigrams.set(bigram, count - 1); intersectionSize++; } } return (2.0 * intersectionSize) / (first.length + second.length - 2); }