diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/dynamic-skills.js b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/dynamic-skills.js new file mode 100644 index 00000000..6b6de66d --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/dynamic-skills.js @@ -0,0 +1,157 @@ +"use strict"; + +// Dynamically load skills after BE startup. +// This avoids needing to rebuild the big BE bundle when adding new skills on disk. + +const path = require("path"); +const jibo = require("jibo"); + +let rlog = null; +try { + rlog = require("./robot-logger"); +} catch (e) { + rlog = null; +} + +function log(msg, data) { + const line = "[DYN-SKILLS] " + msg; + try { + console.log(line, data || ""); + } catch (e) {} + try { + if (rlog && typeof rlog.raw === "function") rlog.raw(line + (data ? " " + JSON.stringify(data) : "")); + } catch (e) {} +} + +function normalizeSkillExport(SkillExport, id) { + if (typeof SkillExport === "function") return SkillExport; + if (SkillExport && typeof SkillExport.Skill === "function") return SkillExport.Skill; + throw new Error("Error loading skill: " + id + ". Incorrect exports"); +} + +function attachLifecycleHooks(be, id, skill) { + // Mirror the hooks the Be constructor normally installs. + try { + skill.on("exit", function () { + be.exit.call(be, skill, ...arguments); + }); + skill.on("redirect", function () { + be.skillRedirect.call(be, skill, ...arguments); + }); + skill.on("refresh", function () { + be.skillRedirect.call(be, skill, skill.assetPack, ...arguments); + }); + } catch (e) { + log("failed to attach lifecycle hooks", { id: id, err: String(e && (e.stack || e.message || e)) }); + } + + const empty = (done) => { + try { + done(); + } catch (e) {} + }; + + try { + if (!skill.postInit) skill.postInit = empty; + if (!skill.preload) skill.preload = empty; + } catch (e) {} +} + +function tryLoadSkill(be, id) { + if (!be || !be.skills) throw new Error("be.skills missing"); + if (!id || typeof id !== "string") return false; + + if (be.skills[id]) return true; + + // eslint-disable-next-line global-require, import/no-dynamic-require + const SkillExport = require(id); + const Skill = normalizeSkillExport(SkillExport, id); + + const rootPath = path.dirname(jibo.utils.PathUtils.resolve(id)); + const skill = new Skill({ assetPack: id, rootPath: rootPath }); + + if (typeof be._validateSkill === "function" && !be._validateSkill(skill)) { + throw new Error("not a valid BeSkill"); + } + + be.skills[id] = skill; + attachLifecycleHooks(be, id, skill); + + log("loaded", { id: id }); + return true; +} + +function collectSkillIdsFromMenuEntries(entries) { + const ids = new Set(); + + (entries || []).forEach(function (e) { + if (!e) return; + + if (e.type === "skill" && e.skillId) ids.add(e.skillId); + + if (e.type === "submenu" && Array.isArray(e.children)) { + e.children.forEach(function (c) { + if (c && c.type === "skill" && c.skillId) ids.add(c.skillId); + }); + } + }); + + return Array.from(ids); +} + +function loadMissingSkillsFromMenuEntries(be, opts) { + opts = opts || {}; + + let menuEntries = null; + try { + // Note: this is the modular path (includes provider entries). + menuEntries = require("../menu/menu-entries"); + } catch (e) { + log("menu-entries module missing", { err: String(e && (e.stack || e.message || e)) }); + return { loaded: [], failed: [] }; + } + + let res; + try { + res = menuEntries.getMenuEntries({ + skillsRoot: opts.skillsRoot, + providersDir: opts.providersDir, + log: function () { + try { + log(Array.prototype.join.call(arguments, " ")); + } catch (e) {} + } + }); + } catch (e) { + log("getMenuEntries failed", { err: String(e && (e.stack || e.message || e)) }); + return { loaded: [], failed: [] }; + } + + const ids = collectSkillIdsFromMenuEntries(res && res.entries); + log("menu referenced skillIds", { count: ids.length }); + + const loaded = []; + const failed = []; + + ids.forEach(function (id) { + try { + // Only attempt ids that look like NPM packages (reduces noise). + if (typeof id !== "string") return; + if (id.indexOf("/") === -1) return; + + if (!be.skills[id]) { + tryLoadSkill(be, id); + loaded.push(id); + } + } catch (e) { + failed.push({ id: id, err: String(e && (e.stack || e.message || e)) }); + log("load failed", { id: id, err: String(e && (e.message || e)) }); + } + }); + + return { loaded: loaded, failed: failed, providersDir: res && res.providersDir, skillsRoot: res && res.skillsRoot }; +} + +module.exports = { + loadMissingSkillsFromMenuEntries: loadMissingSkillsFromMenuEntries +}; diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/postinit.js b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/postinit.js index c9641c1b..5532e3a4 100644 --- a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/postinit.js +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/postinit.js @@ -54,6 +54,26 @@ exports.postInit = function (err) { this.log.warn('Dynamic skills patch failed (non-fatal):', e.message || e); } + // Optional: dynamically load skills referenced by modular menu entries. + // This is the missing piece for “menu entry exists” => “skill is launchable”. + try { + const dynSkills = require('./dynamic-skills'); + const res = dynSkills.loadMissingSkillsFromMenuEntries(this); + try { + this.log.info('Dynamic skill load complete', { + loaded: res && res.loaded ? res.loaded.length : 0, + failed: res && res.failed ? res.failed.length : 0, + skillsRoot: res && res.skillsRoot, + providersDir: res && res.providersDir + }); + } catch (e2) { + // ignore + } + } + catch (e) { + this.log.warn('Dynamic skill loader failed (non-fatal):', e.message || e); + } + // Optional: AI Bridge (modular; can run models off-robot for now) try { if (rlog && typeof rlog.raw === 'function') { diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/TASKS.md b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/TASKS.md new file mode 100644 index 00000000..aa33483c --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/TASKS.md @@ -0,0 +1,44 @@ +# Plex Music (Jibo) — task list + +Goal: add a Jibo menu button that opens a Plex music browser UI. Next phase will be playback. + +## Phase 0 — Get a basic GUI up (now) +- [x] Create launchable skill module `@be/plex-music` +- [x] Add modular menu entry in `@be/menu-entries.d/` +- [x] Show a simple `MenuView` with placeholder actions + +## Phase 1 — Connect to Plex (discovery + auth) +- [ ] Decide config source for server + token + - Option A: JSON file on-robot (e.g. `/opt/jibo/Jibo/Skills/@be/plex-config.json`) + - Option B: env vars (`PLEX_BASE_URL`, `PLEX_TOKEN`) + - Option C: run a tiny local helper service and talk to it +- [ ] Implement a minimal Plex client (HTTP GET) + - [ ] Fetch `/:/resources` or `/?X-Plex-Token=...` health check + - [ ] Handle HTTPS vs HTTP and timeouts +- [ ] Add a “connection status” indicator in the UI +- [ ] Log failures to robot logd (UDP) for easy debugging + +## Phase 2 — Browse music library (read-only) +- [ ] List music libraries (Plex sections) +- [ ] Pick a library (default to first music section) +- [ ] Browse hierarchy (at minimum) + - [ ] Artists list + - [ ] Albums for artist + - [ ] Tracks for album +- [ ] Add simple paging/scroll behavior (Jibo screen limits) +- [ ] Cache results in-memory for snappy navigation + +## Phase 3 — Playback (next) +- [ ] Decide playback strategy + - Option A: stream directly and use Jibo audio APIs + - Option B: ask Plex to transcode to a simple stream + - Option C: route through an on-robot helper (Python) that downloads/streams +- [ ] Add “Play / Pause / Next” UI +- [ ] Maintain a queue (album or playlist) +- [ ] Show “Now Playing” (track title / artist / album) + +## Phase 4 — Polish +- [ ] Better icons + labels +- [ ] Remember last-used library and last selection +- [ ] Handle token expiration gracefully +- [ ] Add a tiny test harness (run on dev box against Plex) diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.html b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.html new file mode 100644 index 00000000..9d5ddff8 --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.html @@ -0,0 +1,12 @@ + + + + + + Plex Music + + +
+ + + diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js new file mode 100644 index 00000000..3a2c3a70 --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js @@ -0,0 +1,128 @@ +"use strict"; + +const jibo = require("jibo"); +const beFramework = require("@be/be-framework"); + +let rlog = null; +try { + // Optional: log to UDP logd if available + rlog = require("@be/be/be/robot-logger"); +} catch (e) { + rlog = null; +} + +function log(line, data) { + const msg = `[plex-music] ${line}`; + try { + console.log(msg, data || ""); + } catch (e) {} + try { + if (rlog && typeof rlog.info === "function") { + rlog.info("plex-music", line, data || undefined); + } + } catch (e) {} +} + +class PlexMusic extends beFramework.BeSkill { + constructor(assetPack) { + super(assetPack); + this._menuView = null; + this._onSelect = this._onSelect.bind(this); + } + + open(result, refresh) { + log("open", { refresh: !!refresh }); + + const changeViewOptions = { + addView: "resources/views/menu.json", + transitionOpen: jibo.face.views.UP, + transitionClose: jibo.face.views.UP + }; + + const onComplete = () => { + log("menu view complete"); + }; + + const onFailure = (err) => { + log("changeView failure", { err: String(err && (err.stack || err.message || err)) }); + try { + this.exit(); + } catch (e) {} + }; + + const onLoaded = (view) => { + this._menuView = view || null; + try { + if (this._menuView && typeof this._menuView.on === "function") { + this._menuView.on("select", this._onSelect); + } + } catch (e) { + log("failed to bind select handler", { err: String(e && (e.stack || e.message || e)) }); + } + }; + + try { + jibo.face.views.changeView(changeViewOptions, onComplete, onFailure, onLoaded); + } catch (e) { + onFailure(e); + } + } + + _onSelect(selection) { + const id = selection && (selection.id || (selection.data && selection.data.id)); + log("select", { id: id }); + + if (id === "back") { + try { + this.exit(); + } catch (e) {} + return; + } + + // For now this is just a GUI stub; real Plex logic comes next. + if (id === "connect") { + log("connect placeholder"); + return; + } + + if (id === "browse") { + log("browse placeholder"); + return; + } + } + + close(done) { + log("close"); + + try { + if (this._menuView && typeof this._menuView.off === "function") { + this._menuView.off("select", this._onSelect); + } + } catch (e) {} + this._menuView = null; + + const onFailure = (err) => { + log("close changeView failure", { err: String(err && (err.stack || err.message || err)) }); + try { + done(); + } catch (e) {} + }; + + try { + jibo.face.views.changeView( + { + removeAll: true, + leaveEmpty: true, + transitionOpen: jibo.face.views.DOWN, + transitionClose: jibo.face.views.DOWN + }, + () => done(), + onFailure + ); + } catch (e) { + onFailure(e); + } + } +} + +module.exports = PlexMusic; diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/package.json b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/package.json new file mode 100644 index 00000000..ba37465c --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/package.json @@ -0,0 +1,21 @@ +{ + "name": "@be/plex-music", + "version": "0.1.0", + "description": "Plex Music browser (WIP)", + "main": "index.js", + "jibo": { + "main": "index.html", + "type": "asset-pack", + "display-name": "plex-music" + }, + "config": { + "standalone": true + }, + "dependencies": { + "@be/be-framework": "^13.0.0", + "jibo": "^15.0.0" + }, + "engines": { + "node": ">=6.0" + } +} diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/resources/views/menu.json b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/resources/views/menu.json new file mode 100644 index 00000000..571a9aa5 --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/resources/views/menu.json @@ -0,0 +1,33 @@ +{ + "viewConfig": { + "type": "MenuView", + "id": "plexMusicMenu", + "title": "Plex Music", + "listDefault": { + "menuButtonType": "SkillButton", + "colors": ["0x9B59B6", "0x6C3483"] + }, + "list": [ + { + "id": "status", + "label": "Status: not connected", + "iconSrc": "core://resources/actionIcons/cancel.png" + }, + { + "id": "connect", + "label": "Configure Plex server", + "iconSrc": "core://resources/actionIcons/ok.png" + }, + { + "id": "browse", + "label": "Browse library (placeholder)", + "iconSrc": "core://resources/actionIcons/ok.png" + }, + { + "id": "back", + "label": "← Back", + "iconSrc": "core://resources/actionIcons/cancel.png" + } + ] + } +} diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/package.json b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/package.json index 426ad0ec..ca00c51f 100644 --- a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/package.json +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/package.json @@ -33,6 +33,7 @@ "@be/introductions", "@be/remote", "@be/nimbus", + "@be/plex-music", "@be/restore", "@be/surprises", "@be/surprises-date", diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/menu-entries.d/20-plex-music.json b/V3.1/build/opt/jibo/Jibo/Skills/@be/menu-entries.d/20-plex-music.json new file mode 100644 index 00000000..3e9f0314 --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/menu-entries.d/20-plex-music.json @@ -0,0 +1,12 @@ +[ + { + "id": "plex-music", + "type": "skill", + "title": "Plex Music", + "icon": "resources/icons/radio.png", + "color": "purple", + "skillId": "@be/plex-music", + "description": "Browse your Plex music library (WIP)", + "order": 55 + } +] diff --git a/V3.1/mode.json b/V3.1/mode.json new file mode 100644 index 00000000..4a38eacf --- /dev/null +++ b/V3.1/mode.json @@ -0,0 +1 @@ +{"mode": "normal"}