Version 3.1 InDev

This commit is contained in:
2026-03-16 19:20:27 +02:00
parent 81e6e0a7a2
commit d7a6f43af1
224 changed files with 2168 additions and 14011 deletions

View File

@@ -0,0 +1,60 @@
"use strict";
// Minimal UDP logger client for old robot environments.
// Safe to require from any module; if logd is not running, it will just no-op.
const dgram = require("dgram");
const DEFAULT_HOST = process.env.JIBO_LOGD_HOST || "127.0.0.1";
const DEFAULT_PORT = parseInt(process.env.JIBO_LOGD_PORT || "15140", 10);
let _socket = null;
function getSocket() {
if (_socket) return _socket;
try {
_socket = dgram.createSocket("udp4");
_socket.unref();
return _socket;
} catch (e) {
return null;
}
}
function send(obj) {
const sock = getSocket();
if (!sock) return;
let payload;
try {
payload = Buffer.from(JSON.stringify(obj));
} catch (e) {
return;
}
try {
sock.send(payload, 0, payload.length, DEFAULT_PORT, DEFAULT_HOST, function () { });
} catch (e) {
// ignore
}
}
function mk(tag, level, msg, data) {
const o = { tag: tag || "skill", level: level || "info", msg: String(msg || "") };
if (typeof data !== "undefined") o.data = data;
return o;
}
exports.info = function (tag, msg, data) { send(mk(tag, "info", msg, data)); };
exports.warn = function (tag, msg, data) { send(mk(tag, "warn", msg, data)); };
exports.error = function (tag, msg, data) { send(mk(tag, "error", msg, data)); };
exports.debug = function (tag, msg, data) { send(mk(tag, "debug", msg, data)); };
exports.raw = function (line) {
const sock = getSocket();
if (!sock) return;
const payload = Buffer.from(String(line || ""));
try {
sock.send(payload, 0, payload.length, DEFAULT_PORT, DEFAULT_HOST, function () { });
} catch (e) { }
};

View File

@@ -0,0 +1,65 @@
# Menu providers (drop-in menu customization)
Providers let you add/override menu entries without editing the core menu patch.
Default entries directory (on robot):
- `/opt/jibo/Jibo/Skills/@be/menu-entries.d/`
Legacy fallback (yea ik its the same stop talking):
- `/opt/jibo/Jibo/Skills/@be/menu-providers.d/`
## Provider file types
### JSON provider (`*.json`)
Supported shapes:
- An array of entries: `[ { ... }, { ... } ]`
- Or `{ "entries": [ ... ] }`
Entry schema (same as `menuEntry.json` scan output):
- `id` (string, required)
- `type` (`skill` or `submenu`)
- `title`
- `icon`
- `color`
- `description`
- `order` (number)
- `skillId` (for type `skill`)
- `submenuTitle` (for type `submenu`)
- `children` (array of skill entries, for type `submenu`)
- `childrenDir` (for type `submenu`, optional):
- Absolute path (starts with `/`) or relative to `skillsRoot`
- If provided and `children` is missing/empty, the patch will scan this directory for child skills (subfolders containing `menuEntry.json`).
Example submenu that lists a directory:
```json
[
{
"id": "fun_stuff",
"type": "submenu",
"title": "Fun Stuff",
"icon": "resources/icons/fun-stuff.png",
"order": 20,
"childrenDir": "FunStuff"
}
]
```
### JS provider (`*.js`)
Exports one of:
- `module.exports = function(ctx) { return [ ...entries... ]; }`
- `exports.getEntries = function(ctx) { return [ ...entries... ]; }`
- `exports.entries = [ ...entries... ]`
`ctx` includes:
- `skillsRoot`
- `providersDir`
- `log` (function)
## Conflict rules
- If a provider entry has the same `id` as a scanned entry, the provider entry wins.
- Sorting: by `order` then by title.

View File

@@ -12,7 +12,7 @@ When Jibo boots up, the `main-menu-patch.js` module:
## Quick Start
### Adding a Simple Skill Button to the Menu
### Adding a Simple Skill Button to the Menu (you really should follow PROVIDERS.md at this point)
1. Create a folder in the `Skills` directory (e.g., `MySkill/`)
2. Add a `menuEntry.json` file inside:
@@ -53,7 +53,7 @@ When Jibo boots up, the `main-menu-patch.js` module:
3. Inside that folder, create subfolders for each skill, each with their own `menuEntry.json`
4. A button will appear in the main menu that opens a submenu with those skills!
## Directory Structure Example
## Directory Structure Example (OLD)
```
/opt/jibo/Jibo/Skills/

View File

@@ -10,11 +10,38 @@
const fs = require('fs');
const path = require('path');
// Skills root directory
const SKILLS_ROOT = '/opt/jibo/Jibo/Skills';
const menuEntries = require('./menu-entries');
const skillsRootUtil = require('./skills-root');
function strStartsWith(s, prefix) {
return (typeof s === 'string') && (s.indexOf(prefix) === 0);
}
function strEndsWith(s, suffix) {
if (typeof s !== 'string' || typeof suffix !== 'string') return false;
if (suffix.length > s.length) return false;
return s.indexOf(suffix, s.length - suffix.length) !== -1;
}
function strIncludes(s, needle) {
return (typeof s === 'string') && (s.indexOf(needle) !== -1);
}
// Optional UDP logger (Python logd). Safe fallback if unavailable.
let robotLogger = null;
try {
robotLogger = require('../be/robot-logger');
} catch (e) {
robotLogger = null;
}
// Skills root directory (configurable)
let SKILLS_ROOT = skillsRootUtil.resolveSkillsRoot();
// Cache for scanned skills
let _cachedMenuEntries = null;
let _cachedSkillsRoot = null;
let _cachedProvidersDir = null;
let _submenuConfigs = {};
let _dynamicSkillIds = new Set(); // Track dynamic skill IDs for routing
let _beInstance = null;
@@ -40,6 +67,11 @@ const LOG_FILE = '/tmp/menu-patch.log';
function log(msg, ...args) {
const line = '[MENU-PATCH] ' + msg + ' ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
console.log(line);
try {
if (robotLogger && typeof robotLogger.raw === 'function') {
robotLogger.raw(line);
}
} catch (e) { }
try {
fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + line + '\n');
} catch (e) {}
@@ -48,6 +80,11 @@ function log(msg, ...args) {
function warn(msg, ...args) {
const line = '[MENU-PATCH WARN] ' + msg + ' ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
console.warn(line);
try {
if (robotLogger && typeof robotLogger.raw === 'function') {
robotLogger.raw(line);
}
} catch (e) { }
try {
fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + line + '\n');
} catch (e) {}
@@ -172,59 +209,37 @@ function scanSubmenuChildren(submenuDir) {
* Scan the Skills root directory for all menu entries
*/
function scanAllMenuEntries() {
if (_cachedMenuEntries) {
if (_cachedMenuEntries && _cachedSkillsRoot === SKILLS_ROOT) {
log('Returning cached entries:', _cachedMenuEntries.length);
return _cachedMenuEntries;
}
log('Scanning Skills directory:', SKILLS_ROOT);
_dynamicSkillIds.clear();
const entries = [];
let children;
// Check if directory exists
if (!fs.existsSync(SKILLS_ROOT)) {
warn('Skills directory does not exist:', SKILLS_ROOT);
return entries;
}
const res = menuEntries.getMenuEntries({
skillsRoot: SKILLS_ROOT,
log: function () {
try { log.apply(null, arguments); } catch (e) { }
}
});
_cachedMenuEntries = res.entries || [];
_cachedSkillsRoot = res.skillsRoot;
_cachedProvidersDir = res.providersDir;
// update dynamic id set
try {
children = fs.readdirSync(SKILLS_ROOT);
log('Found', children.length, 'items in Skills directory:', children.join(', '));
} catch (e) {
warn('Failed to read skills directory:', SKILLS_ROOT, e.message);
return entries;
if (res.dynamicSkillIds && typeof res.dynamicSkillIds.forEach === 'function') {
res.dynamicSkillIds.forEach(function (id) { _dynamicSkillIds.add(id); });
}
} catch (e) { }
log('Total entries found:', _cachedMenuEntries.length);
if (_cachedProvidersDir) {
log('Providers dir:', _cachedProvidersDir);
}
children.forEach(name => {
if (name.startsWith('.') || name === '@be' || name === 'node_modules') {
log('Skipping:', name);
return;
}
const skillDir = path.join(SKILLS_ROOT, name);
if (!isDirectory(skillDir)) {
log('Not a directory:', name);
return;
}
const entry = scanSkillEntry(skillDir, name);
if (entry) {
entries.push(entry);
log('Added entry:', entry.title, '(type:', entry.type + ')');
}
});
// Sort by order, then alphabetically
entries.sort((a, b) => {
if (a.order !== b.order) return a.order - b.order;
return a.title.localeCompare(b.title);
});
log('Total entries found:', entries.length);
_cachedMenuEntries = entries;
return entries;
return _cachedMenuEntries;
}
/**
@@ -427,7 +442,7 @@ function patchMainMenuSkill(be) {
}
// Handle dynamic submenus
if (skill && skill.startsWith('dynamic-submenu-')) {
if (skill && strStartsWith(skill, 'dynamic-submenu-')) {
log('Showing submenu:', skill);
const submenuConfig = _submenuConfigs[skill];
if (submenuConfig && _jibo && _jibo.face && _jibo.face.views) {
@@ -443,11 +458,15 @@ function patchMainMenuSkill(be) {
if (result && result.on) {
result.on('select', (selection) => {
log('Submenu item selected:', selection);
if (selection && selection.action && selection.action.data) {
const destination = selection.action.data.utterance?.entities?.destination;
if (destination) {
this.redirectToSkill(destination);
try {
if (selection && selection.action && selection.action.data && selection.action.data.utterance && selection.action.data.utterance.entities) {
const destination = selection.action.data.utterance.entities.destination;
if (destination) {
this.redirectToSkill(destination);
}
}
} catch (e) {
warn('Submenu selection handler error:', e && e.message ? e.message : e);
}
});
}
@@ -459,7 +478,7 @@ function patchMainMenuSkill(be) {
}
// Handle dynamic skills (prefixed with 'dynamic:')
if (skill && skill.startsWith('dynamic:')) {
if (skill && strStartsWith(skill, 'dynamic:')) {
const actualSkillId = skill.substring(8); // Remove 'dynamic:' prefix
log('Launching dynamic skill:', actualSkillId);
@@ -530,6 +549,14 @@ function applyPatch(be) {
function patchLoader(jibo, skillsRoot) {
log('patchLoader called');
_jibo = jibo;
// Allow caller to override skills root.
if (skillsRoot && typeof skillsRoot === 'string') {
SKILLS_ROOT = skillsRoot;
} else {
SKILLS_ROOT = skillsRootUtil.resolveSkillsRoot();
}
clearCache();
if (!jibo) {
warn('jibo is null/undefined');
@@ -553,7 +580,7 @@ function patchLoader(jibo, skillsRoot) {
log('>>> jibo.loader.load called with:', resourcePath);
// Check if this is loading a dynamic submenu
if (resourcePath && resourcePath.startsWith('dynamic-submenu-')) {
if (resourcePath && strStartsWith(resourcePath, 'dynamic-submenu-')) {
const submenuConfig = _submenuConfigs[resourcePath];
if (submenuConfig) {
log('Loading dynamic submenu:', resourcePath);
@@ -566,10 +593,10 @@ function patchLoader(jibo, skillsRoot) {
// Check if this is the main menu - match various possible paths
const isMainMenu = resourcePath && (
resourcePath.includes('main-menu-verbal.json') ||
resourcePath.includes('main-menu.json') ||
strIncludes(resourcePath, 'main-menu-verbal.json') ||
strIncludes(resourcePath, 'main-menu.json') ||
resourcePath === 'resources/views/main-menu-verbal.json' ||
resourcePath.endsWith('main-menu-verbal.json')
strEndsWith(resourcePath, 'main-menu-verbal.json')
);
if (isMainMenu) {
@@ -584,12 +611,22 @@ function patchLoader(jibo, skillsRoot) {
}
log('Original menu config loaded successfully');
log('Original button count:', config?.viewConfig?.list?.length || 0);
try {
const origCount = (config && config.viewConfig && config.viewConfig.list && config.viewConfig.list.length) ? config.viewConfig.list.length : 0;
log('Original button count:', origCount);
} catch (e) {
log('Original button count: (unknown)');
}
// Inject dynamic skills
const patchedConfig = injectDynamicSkills(config);
log('Patched button count:', patchedConfig?.viewConfig?.list?.length || 0);
try {
const patchedCount = (patchedConfig && patchedConfig.viewConfig && patchedConfig.viewConfig.list && patchedConfig.viewConfig.list.length) ? patchedConfig.viewConfig.list.length : 0;
log('Patched button count:', patchedCount);
} catch (e) {
log('Patched button count: (unknown)');
}
if (callback) callback(null, patchedConfig);
});
@@ -608,6 +645,8 @@ function patchLoader(jibo, skillsRoot) {
*/
function clearCache() {
_cachedMenuEntries = null;
_cachedSkillsRoot = null;
_cachedProvidersDir = null;
_submenuConfigs = {};
_dynamicSkillIds.clear();
}

View File

@@ -0,0 +1,72 @@
"use strict";
const fs = require("fs");
const path = require("path");
const skillsRoot = require("./skills-root");
const scanner = require("./menu-entry-scanner");
const providers = require("./menu-providers");
function isDirectory(p) {
try {
return fs.statSync(p).isDirectory();
} catch (e) {
return false;
}
}
function expandSubmenuChildren(entries, root, opts) {
(entries || []).forEach(function (e) {
if (!e || e.type !== "submenu") return;
if (e.children && e.children.length) return;
if (!e.childrenDir) return;
let dir = e.childrenDir;
if (typeof dir !== "string" || dir.trim().length === 0) return;
dir = dir.trim();
const resolved = (dir.charAt(0) === "/") ? dir : path.join(root, dir);
if (!isDirectory(resolved)) {
if (opts && opts.log) opts.log("submenu childrenDir not a directory", e.id, resolved);
e.children = [];
return;
}
if (opts && opts.log) opts.log("submenu childrenDir scan", e.id, resolved);
e.children = scanner.scanSubmenuChildren(resolved, { log: opts && opts.log, defaultIcon: opts && opts.defaultIcon, defaultColor: opts && opts.defaultColor, defaultOrder: opts && opts.defaultOrder });
});
}
function getMenuEntries(opts) {
opts = opts || {};
const root = skillsRoot.resolveSkillsRoot(opts.skillsRoot);
const providerDir = opts.providersDir || skillsRoot.providersDirForRoot(root);
const scanned = scanner.scanAllMenuEntries(root, {
log: opts.log,
defaultIcon: opts.defaultIcon,
defaultColor: opts.defaultColor,
defaultOrder: opts.defaultOrder
});
const provided = providers.loadProviderEntries(providerDir, { log: opts.log, skillsRoot: root, providersDir: providerDir });
const merged = providers.mergeById(scanned, provided);
// Allow provider-defined submenus to be backed by an arbitrary directory.
// Example: { type: "submenu", id: "fun", childrenDir: "FunStuff" }
expandSubmenuChildren(merged, root, opts);
const dynamicIds = scanner.collectDynamicSkillIds(merged);
return {
skillsRoot: root,
providersDir: providerDir,
entries: merged,
dynamicSkillIds: dynamicIds
};
}
module.exports = {
getMenuEntries: getMenuEntries
};

View File

@@ -0,0 +1,150 @@
"use strict";
const fs = require("fs");
const path = require("path");
function safeReadJson(filePath, logFn) {
try {
const txt = fs.readFileSync(filePath, "utf8");
if (!txt || txt.trim().length === 0) return null;
return JSON.parse(txt);
} catch (e) {
if (logFn) logFn("safeReadJson failed", filePath, e && e.message ? e.message : e);
return null;
}
}
function isDirectory(p) {
try {
return fs.statSync(p).isDirectory();
} catch (e) {
return false;
}
}
function scanSkillEntry(skillDir, folderName, opts) {
opts = opts || {};
const menuJsonPath = path.join(skillDir, "menuEntry.json");
const menuJson = safeReadJson(menuJsonPath, opts.log);
if (!menuJson) return null;
if (menuJson.hidden === true) return null;
const entry = {
id: folderName,
type: menuJson.type || "skill",
title: menuJson.title || menuJson.label || folderName,
icon: menuJson.icon || menuJson.iconSrc || (opts.defaultIcon || "resources/icons/settings.png"),
color: menuJson.color || menuJson.colors || (opts.defaultColor || "default"),
description: menuJson.description || "",
path: skillDir,
order: typeof menuJson.order === "number" ? menuJson.order : (typeof opts.defaultOrder === "number" ? opts.defaultOrder : 100),
skillId: menuJson.skillId || folderName,
submenuTitle: menuJson.submenuTitle || menuJson.title || folderName
};
if (entry.type === "submenu") {
entry.children = scanSubmenuChildren(skillDir, opts);
}
// Legacy support
if (menuJson.isSubmenu === true && entry.type !== "submenu") {
entry.type = "submenu";
entry.children = scanSubmenuChildren(skillDir, opts);
}
return entry;
}
function scanSubmenuChildren(submenuDir, opts) {
const children = [];
let items;
try {
items = fs.readdirSync(submenuDir);
} catch (e) {
return children;
}
items.forEach(function (name) {
if (!name || name.charAt(0) === ".") return;
if (name === "@be" || name === "node_modules") return;
const childPath = path.join(submenuDir, name);
if (!isDirectory(childPath)) return;
const entry = scanSkillEntry(childPath, name, opts);
if (entry && entry.type === "skill") {
children.push(entry);
}
});
children.sort(function (a, b) {
if (a.order !== b.order) return a.order - b.order;
return String(a.title || "").localeCompare(String(b.title || ""));
});
return children;
}
function scanAllMenuEntries(skillsRoot, opts) {
opts = opts || {};
const entries = [];
if (!skillsRoot || !fs.existsSync(skillsRoot)) {
if (opts.log) opts.log("skills root missing", skillsRoot);
return entries;
}
let children;
try {
children = fs.readdirSync(skillsRoot);
} catch (e) {
if (opts.log) opts.log("failed to read skills root", skillsRoot, e && e.message ? e.message : e);
return entries;
}
children.forEach(function (name) {
if (!name || name.charAt(0) === ".") return;
if (name === "@be" || name === "node_modules") return;
const skillDir = path.join(skillsRoot, name);
if (!isDirectory(skillDir)) return;
const entry = scanSkillEntry(skillDir, name, opts);
if (entry) entries.push(entry);
});
entries.sort(function (a, b) {
if (a.order !== b.order) return a.order - b.order;
return String(a.title || "").localeCompare(String(b.title || ""));
});
return entries;
}
function collectDynamicSkillIds(entries) {
const ids = new Set();
(entries || []).forEach(function (e) {
if (!e) return;
if (e.type === "skill") {
if (e.skillId) ids.add(e.skillId);
if (e.id) ids.add(e.id);
}
if (e.type === "submenu" && e.children && e.children.length) {
e.children.forEach(function (c) {
if (!c) return;
if (c.skillId) ids.add(c.skillId);
if (c.id) ids.add(c.id);
});
}
});
return ids;
}
module.exports = {
scanAllMenuEntries: scanAllMenuEntries,
scanSubmenuChildren: scanSubmenuChildren,
collectDynamicSkillIds: collectDynamicSkillIds
};

View File

@@ -0,0 +1,115 @@
"use strict";
const fs = require("fs");
const path = require("path");
function isDirectory(p) {
try {
return fs.statSync(p).isDirectory();
} catch (e) {
return false;
}
}
function safeReadJson(filePath, logFn) {
try {
const txt = fs.readFileSync(filePath, "utf8");
if (!txt || txt.trim().length === 0) return null;
return JSON.parse(txt);
} catch (e) {
if (logFn) logFn("provider json read failed", filePath, e && e.message ? e.message : e);
return null;
}
}
function loadFromJson(filePath, ctx) {
const obj = safeReadJson(filePath, ctx && ctx.log);
if (!obj) return [];
if (Array.isArray(obj)) return obj;
if (obj && Array.isArray(obj.entries)) return obj.entries;
if (obj && Array.isArray(obj.buttons)) return obj.buttons;
return [];
}
function loadFromJs(filePath, ctx) {
try {
// eslint-disable-next-line global-require, import/no-dynamic-require
const mod = require(filePath);
if (!mod) return [];
if (typeof mod === "function") return mod(ctx) || [];
if (typeof mod.getEntries === "function") return mod.getEntries(ctx) || [];
if (Array.isArray(mod.entries)) return mod.entries;
return [];
} catch (e) {
if (ctx && ctx.log) ctx.log("provider js load failed", filePath, e && e.message ? e.message : e);
return [];
}
}
function loadProviderEntries(providersDir, ctx) {
const out = [];
if (!providersDir || !fs.existsSync(providersDir) || !isDirectory(providersDir)) {
return out;
}
let files;
try {
files = fs.readdirSync(providersDir);
} catch (e) {
return out;
}
files.sort();
files.forEach(function (name) {
if (!name || name.charAt(0) === ".") return;
const full = path.join(providersDir, name);
if (isDirectory(full)) return;
if (name.indexOf(".json", name.length - 5) !== -1) {
const entries = loadFromJson(full, ctx);
entries.forEach(function (e) { out.push(e); });
return;
}
if (name.indexOf(".js", name.length - 3) !== -1) {
const entries = loadFromJs(full, ctx);
entries.forEach(function (e) { out.push(e); });
}
});
return out;
}
// Merge provider entries into scanned entries by id.
// Provider entries win on conflicts.
function mergeById(scannedEntries, providerEntries) {
const byId = {};
(scannedEntries || []).forEach(function (e) {
if (!e || !e.id) return;
byId[e.id] = e;
});
(providerEntries || []).forEach(function (p) {
if (!p || !p.id) return;
byId[p.id] = p;
});
const merged = Object.keys(byId).map(function (k) { return byId[k]; });
merged.sort(function (a, b) {
const ao = typeof a.order === "number" ? a.order : 100;
const bo = typeof b.order === "number" ? b.order : 100;
if (ao !== bo) return ao - bo;
return String(a.title || a.label || a.id).localeCompare(String(b.title || b.label || b.id));
});
return merged;
}
module.exports = {
loadProviderEntries: loadProviderEntries,
mergeById: mergeById
};

View File

@@ -0,0 +1,31 @@
"use strict";
const path = require("path");
const fs = require("fs");
const DEFAULT_ROOT = "/opt/jibo/Jibo/Skills";
function resolveSkillsRoot(overrideRoot) {
if (overrideRoot && typeof overrideRoot === "string") return overrideRoot;
if (process && process.env && process.env.JIBO_SKILLS_ROOT) return process.env.JIBO_SKILLS_ROOT;
return DEFAULT_ROOT;
}
function providersDirForRoot(skillsRoot) {
// Keep providers inside Skills so they can be synced easily.
// Default: /opt/jibo/Jibo/Skills/@be/menu-entries.d
// Legacy fallback: /opt/jibo/Jibo/Skills/@be/menu-providers.d
const v2 = path.join(skillsRoot, "@be", "menu-entries.d");
const v1 = path.join(skillsRoot, "@be", "menu-providers.d");
try {
if (fs.existsSync(v2)) return v2;
if (fs.existsSync(v1)) return v1;
} catch (e) { /* ignore */ }
return v2;
}
module.exports = {
DEFAULT_ROOT: DEFAULT_ROOT,
resolveSkillsRoot: resolveSkillsRoot,
providersDirForRoot: providersDirForRoot
};

View File

@@ -0,0 +1,78 @@
const EventEmitter = require('events');
class LocalRadioPlayer extends EventEmitter {
constructor() {
super();
// These are example streams. You will need to set up your own
// Icecast/HTTP streams and replace these URLs with your server's IP.
this._stations = {
'Rock': 'http://192.168.1.100:8000/rock',
'Pop': 'http://192.168.1.100:8000/pop',
'Jazz': 'http://192.168.1.100:8000/jazz',
'Classical': 'http://192.168.1.100:8000/classical',
'Electronic': 'http://192.168.1.100:8000/electronic',
'Hip-Hop': 'http://192.168.1.100:8000/hiphop'
};
this._audio = null;
console.log("LocalRadioPlayer initialized");
}
getStations(options) {
console.log("LocalRadioPlayer: getStations called");
const stationList = Object.keys(this._stations).map(genre => ({
name: genre,
id: genre
}));
return Promise.resolve(stationList);
}
play(stationData) {
console.log(`LocalRadioPlayer: play called with`, stationData);
if (this._audio) {
this._audio.pause();
}
const streamUrl = this._stations[stationData.id];
if (!streamUrl) {
const err = new Error(`Station ${stationData.id} not found.`);
console.error("LocalRadioPlayer Error:", err);
this.emit('error', err);
return;
}
console.log(`LocalRadioPlayer: Playing stream from ${streamUrl}`);
this._audio = new Audio(streamUrl);
this._audio.addEventListener('error', (e) => {
console.error('LocalRadioPlayer Audio Error:', e);
this.emit('error', new Error('Audio playback failed.'));
});
this._audio.play()
.then(() => {
console.log("LocalRadioPlayer: Playback started.");
this.emit('song-data', {
title: `Streaming ${stationData.id}`,
artist: 'Local Radio',
albumArt: '' // No artwork for local streams
});
})
.catch(err => {
console.error("LocalRadioPlayer Playback Error:", err);
this.emit('error', err);
});
}
stop() {
console.log("LocalRadioPlayer: stop called");
if (this._audio) {
this._audio.pause();
this._audio = null;
}
}
resizeArtwork(options) {
// Not applicable for local streaming
return Promise.resolve('');
}
}
module.exports = LocalRadioPlayer;

View File

@@ -0,0 +1,73 @@
const EventEmitter = require('events');
class LocalRadioPlayer extends EventEmitter {
constructor() {
super();
// These are example streams. You will need to set up your own
// Icecast/HTTP streams and replace these URLs.
this._stations = {
'My Station': 'http://192.168.1.5:6767/Ninja%20Tuna.mp3'
};
this._audio = null;
console.log("LocalRadioPlayer initialized");
}
getStations(options) {
console.log("LocalRadioPlayer: getStations called");
const stationList = Object.keys(this._stations).map(genre => ({
name: genre,
id: genre
}));
return Promise.resolve(stationList);
}
play(stationData) {
console.log(`LocalRadioPlayer: play called with`, stationData);
if (this._audio) {
this._audio.pause();
}
const streamUrl = this._stations[stationData.id];
if (!streamUrl) {
const err = new Error(`Station ${stationData.id} not found.`);
console.error("LocalRadioPlayer Error:", err);
this.emit('error', err);
return;
}
console.log(`LocalRadioPlayer: Playing stream from ${streamUrl}`);
this._audio = new Audio(streamUrl);
this._audio.addEventListener('error', (e) => {
console.error('LocalRadioPlayer Audio Error:', e);
this.emit('error', new Error('Audio playback failed.'));
});
this._audio.play()
.then(() => {
console.log("LocalRadioPlayer: Playback started.");
this.emit('song-data', {
title: `Streaming ${stationData.id}`,
artist: 'Local Radio',
albumArt: '' // No artwork for local streams
});
})
.catch(err => {
console.error("LocalRadioPlayer Playback Error:", err);
this.emit('error', err);
});
}
stop() {
console.log("LocalRadioPlayer: stop called");
if (this._audio) {
this._audio.pause();
this._audio = null;
}
}
resizeArtwork(options) {
// Not applicable for local streaming
return Promise.resolve('');
}
}
module.exports = LocalRadioPlayer;

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB