Compare commits
2 Commits
52de443263
...
7af2a8a7e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
7af2a8a7e0
|
|||
|
b5abe340c8
|
157
V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/dynamic-skills.js
Normal file
157
V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/dynamic-skills.js
Normal file
@@ -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
|
||||
};
|
||||
@@ -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') {
|
||||
|
||||
44
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/TASKS.md
generated
vendored
Normal file
44
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/TASKS.md
generated
vendored
Normal file
@@ -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)
|
||||
12
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.html
generated
vendored
Normal file
12
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.html
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Plex Music</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="face"></div>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
128
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js
generated
vendored
Normal file
128
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js
generated
vendored
Normal file
@@ -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;
|
||||
21
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/package.json
generated
vendored
Normal file
21
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/package.json
generated
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
33
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/resources/views/menu.json
generated
vendored
Normal file
33
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/resources/views/menu.json
generated
vendored
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
"@be/introductions",
|
||||
"@be/remote",
|
||||
"@be/nimbus",
|
||||
"@be/plex-music",
|
||||
"@be/restore",
|
||||
"@be/surprises",
|
||||
"@be/surprises-date",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
1
V3.1/mode.json
Normal file
1
V3.1/mode.json
Normal file
@@ -0,0 +1 @@
|
||||
{"mode": "normal"}
|
||||
Reference in New Issue
Block a user