roonpipe 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -7
- package/data/com.bluemancz.RoonPipe.SearchProvider.ini +5 -0
- package/data/com.bluemancz.RoonPipe.SearchProvider.service +3 -0
- package/data/com.bluemancz.RoonPipe.desktop +8 -0
- package/dist/cli.js +7 -184
- package/dist/gnome-search-provider.js +1 -0
- package/dist/image-cache.js +1 -103
- package/dist/index.js +1 -47
- package/dist/mpris.js +1 -215
- package/dist/notification.js +2 -0
- package/dist/roon.js +1 -265
- package/dist/socket.js +7 -97
- package/package.json +14 -7
- package/scripts/install-gnome-search-provider.sh +37 -0
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ A Linux integration layer for [Roon](https://roonlabs.com/) that brings native d
|
|
|
11
11
|
- **Desktop Notifications** — Get notified when tracks change, complete with album artwork
|
|
12
12
|
- **Playback Controls** — Play, pause, stop, skip, seek, volume, shuffle, and loop
|
|
13
13
|
- **Track Search** — Search your entire Roon library (including TIDAL/Qobuz) via CLI or programmatically
|
|
14
|
+
- **GNOME Search Integration** — Search and play tracks directly from the GNOME overview or search bar
|
|
14
15
|
- **Unix Socket API** — Integrate with other applications using a simple JSON-based IPC protocol
|
|
15
|
-
- **Interactive CLI** — Search and play tracks directly from your terminal
|
|
16
|
+
- **Interactive CLI** — Search and play tracks directly from your terminal with arrow key navigation
|
|
16
17
|
|
|
17
18
|
## CLI Example
|
|
18
19
|
|
|
@@ -59,6 +60,16 @@ pnpm install
|
|
|
59
60
|
pnpm build
|
|
60
61
|
```
|
|
61
62
|
|
|
63
|
+
## GNOME Search Provider
|
|
64
|
+
|
|
65
|
+
To enable searching for tracks directly from the GNOME overview or search bar, install the search provider (requires sudo):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
sudo ./scripts/install-gnome-search-provider.sh
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This will copy the necessary files to system directories and restart the GNOME shell. After installation, you can search for track names in the GNOME search to see RoonPipe results.
|
|
72
|
+
|
|
62
73
|
## Usage
|
|
63
74
|
|
|
64
75
|
### Running the Daemon
|
|
@@ -187,12 +198,14 @@ Available actions:
|
|
|
187
198
|
|
|
188
199
|
```
|
|
189
200
|
src/
|
|
190
|
-
├── index.ts
|
|
191
|
-
├── roon.ts
|
|
192
|
-
├── mpris.ts
|
|
193
|
-
├──
|
|
194
|
-
├──
|
|
195
|
-
|
|
201
|
+
├── index.ts # Entry point, daemon/CLI mode switching
|
|
202
|
+
├── roon.ts # Roon API connection and browsing
|
|
203
|
+
├── mpris.ts # MPRIS player and metadata
|
|
204
|
+
├── notification.ts # Desktop notifications
|
|
205
|
+
├── socket.ts # Unix socket server
|
|
206
|
+
├── image-cache.ts # Album artwork caching
|
|
207
|
+
├── gnome-search-provider.ts # GNOME search provider integration
|
|
208
|
+
└── cli.ts # Interactive terminal interface
|
|
196
209
|
```
|
|
197
210
|
|
|
198
211
|
## Contributing
|
package/dist/cli.js
CHANGED
|
@@ -1,184 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.startCLI = startCLI;
|
|
16
|
-
const node_net_1 = __importDefault(require("node:net"));
|
|
17
|
-
const node_readline_1 = __importDefault(require("node:readline"));
|
|
18
|
-
const prompts_1 = require("@inquirer/prompts");
|
|
19
|
-
const SOCKET_PATH = "/tmp/roonpipe.sock";
|
|
20
|
-
// Helper to send command to daemon via socket
|
|
21
|
-
function sendCommand(command) {
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
const client = node_net_1.default.createConnection(SOCKET_PATH, () => {
|
|
24
|
-
client.write(JSON.stringify(command));
|
|
25
|
-
});
|
|
26
|
-
let data = "";
|
|
27
|
-
client.on("data", (chunk) => {
|
|
28
|
-
data += chunk.toString();
|
|
29
|
-
});
|
|
30
|
-
client.on("end", () => {
|
|
31
|
-
try {
|
|
32
|
-
const response = JSON.parse(data);
|
|
33
|
-
if (response.error) {
|
|
34
|
-
reject(response.error);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
resolve(response);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (_a) {
|
|
41
|
-
reject("Failed to parse response");
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
client.on("error", (err) => {
|
|
45
|
-
reject(`Cannot connect to RoonPipe daemon. Is it running?\n${err.message}`);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
function searchQuery() {
|
|
50
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
-
const rl = node_readline_1.default.createInterface({
|
|
52
|
-
input: process.stdin,
|
|
53
|
-
output: process.stdout,
|
|
54
|
-
});
|
|
55
|
-
return new Promise((resolve) => {
|
|
56
|
-
rl.question("🔍 Search for a track: ", (answer) => {
|
|
57
|
-
rl.close();
|
|
58
|
-
resolve(answer);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
function search() {
|
|
64
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
-
const query = yield searchQuery();
|
|
66
|
-
if (!query.trim()) {
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
console.log(`\nSearching for "${query}"...\n`);
|
|
70
|
-
try {
|
|
71
|
-
const response = yield sendCommand({ command: "search", query });
|
|
72
|
-
return response.results || [];
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
console.error("❌ Error:", error);
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
function selectTrack(results) {
|
|
81
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
82
|
-
const choices = [
|
|
83
|
-
...results.map((result, index) => ({
|
|
84
|
-
name: `${result.title} ${result.subtitle ? `· ${result.subtitle}` : ""}`,
|
|
85
|
-
value: index,
|
|
86
|
-
})),
|
|
87
|
-
new prompts_1.Separator(),
|
|
88
|
-
{ name: "🔍 New search", value: -1 },
|
|
89
|
-
{ name: "❌ Quit", value: -2 },
|
|
90
|
-
];
|
|
91
|
-
try {
|
|
92
|
-
const selection = yield (0, prompts_1.select)({
|
|
93
|
-
message: "Select a track to play:",
|
|
94
|
-
choices,
|
|
95
|
-
pageSize: 15,
|
|
96
|
-
theme: { prefix: "" },
|
|
97
|
-
});
|
|
98
|
-
if (selection === -2) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
if (selection === -1) {
|
|
102
|
-
return { item_key: "", sessionKey: "", title: "", subtitle: "__search__" };
|
|
103
|
-
}
|
|
104
|
-
return results[selection];
|
|
105
|
-
}
|
|
106
|
-
catch (_a) {
|
|
107
|
-
// User pressed Ctrl+C
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
function selectAction() {
|
|
113
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
-
try {
|
|
115
|
-
const action = yield (0, prompts_1.select)({
|
|
116
|
-
message: "What do you want to do?",
|
|
117
|
-
choices: [
|
|
118
|
-
{ name: "▶️ Play now", value: "playNow" },
|
|
119
|
-
{ name: "📋 Add to queue", value: "queue" },
|
|
120
|
-
{ name: "⏭️ Play next", value: "addNext" },
|
|
121
|
-
new prompts_1.Separator(),
|
|
122
|
-
{ name: "← Back", value: "back" },
|
|
123
|
-
],
|
|
124
|
-
theme: { prefix: "" },
|
|
125
|
-
});
|
|
126
|
-
return action === "back" ? null : action;
|
|
127
|
-
}
|
|
128
|
-
catch (_a) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
function playTrack(track, action) {
|
|
134
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
-
const actionLabels = {
|
|
136
|
-
playNow: "▶️ Playing",
|
|
137
|
-
queue: "📋 Added to queue",
|
|
138
|
-
addNext: "⏭️ Playing next",
|
|
139
|
-
};
|
|
140
|
-
console.log(`\n${actionLabels[action]}: ${track.title}${track.subtitle ? ` · ${track.subtitle}` : ""}\n`);
|
|
141
|
-
try {
|
|
142
|
-
yield sendCommand({
|
|
143
|
-
command: "play",
|
|
144
|
-
item_key: track.item_key,
|
|
145
|
-
session_key: track.sessionKey,
|
|
146
|
-
action,
|
|
147
|
-
});
|
|
148
|
-
console.log("✅ Success!\n");
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
console.error("❌ Failed:", error);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
function startCLI() {
|
|
156
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
157
|
-
console.log("\n🎵 RoonPipe Interactive Search");
|
|
158
|
-
console.log("==============================\n");
|
|
159
|
-
let running = true;
|
|
160
|
-
while (running) {
|
|
161
|
-
const results = yield search();
|
|
162
|
-
if (results.length === 0) {
|
|
163
|
-
console.log("❌ No tracks found.\n");
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
console.log(`Found ${results.length} track(s):\n`);
|
|
167
|
-
const selected = yield selectTrack(results);
|
|
168
|
-
if (selected === null) {
|
|
169
|
-
console.log("\nGoodbye! 👋\n");
|
|
170
|
-
running = false;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (selected.subtitle === "__search__") {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
const action = yield selectAction();
|
|
177
|
-
if (action === null) {
|
|
178
|
-
// User chose "Back", continue to track selection
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
yield playTrack(selected, action);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"startCLI",{enumerable:!0,get:function(){return s}});let e=/*#__PURE__*/r(require("node:net")),t=/*#__PURE__*/r(require("node:readline")),n=require("@inquirer/prompts");function r(e){return e&&e.__esModule?e:{default:e}}function o(t){return new Promise((n,r)=>{let o=e.default.createConnection("/tmp/roonpipe.sock",()=>{o.write(JSON.stringify(t))}),a="";o.on("data",e=>{a+=e.toString()}),o.on("end",()=>{try{let e=JSON.parse(a);e.error?r(e.error):n(e)}catch{r("Failed to parse response")}}),o.on("error",e=>{r(`Cannot connect to RoonPipe daemon. Is it running?
|
|
2
|
+
${e.message}`)})})}async function a(){let e=t.default.createInterface({input:process.stdin,output:process.stdout});return new Promise(t=>{e.question("🔍 Search for a track: ",n=>{e.close(),t(n)})})}async function l(){let e=await a();if(!e.trim())return[];console.log(`
|
|
3
|
+
Searching for "${e}"...
|
|
4
|
+
`);try{return(await o({command:"search",query:e})).results||[]}catch(e){return console.error("❌ Error:",e),[]}}async function i(e){let t=[...e.map((e,t)=>({name:`${e.title} ${e.subtitle?`\xb7 ${e.subtitle}`:""}`,value:t})),new n.Separator,{name:"🔍 New search",value:-1},{name:"❌ Quit",value:-2}];try{let r=await (0,n.select)({message:"Select a track to play:",choices:t,pageSize:15,theme:{prefix:""}});if(-2===r)return null;if(-1===r)return{item_key:"",sessionKey:"",title:"",subtitle:"__search__"};return e[r]}catch{return null}}async function u(){try{let e=await (0,n.select)({message:"What do you want to do?",choices:[{name:"▶️ Play now",value:"playNow"},{name:"📋 Add to queue",value:"queue"},{name:"⏭️ Play next",value:"addNext"},new n.Separator,{name:"← Back",value:"back"}],theme:{prefix:""}});return"back"===e?null:e}catch{return null}}async function c(e,t){console.log(`
|
|
5
|
+
${{playNow:"▶️ Playing",queue:"📋 Added to queue",addNext:"⏭️ Playing next"}[t]}: ${e.title}${e.subtitle?` \xb7 ${e.subtitle}`:""}
|
|
6
|
+
`);try{await o({command:"play",item_key:e.item_key,session_key:e.sessionKey,action:t}),console.log("✅ Success!\n")}catch(e){console.error("❌ Failed:",e)}}async function s(){console.log("\n🎵 RoonPipe Interactive Search"),console.log("==============================\n");let e=!0;for(;e;){let t=await l();if(0===t.length){console.log("❌ No tracks found.\n");continue}console.log(`Found ${t.length} track(s):
|
|
7
|
+
`);let n=await i(t);if(null===n){console.log("\nGoodbye! 👋\n"),e=!1;continue}if("__search__"===n.subtitle)continue;let r=await u();null!==r&&await c(n,r)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e;Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"initGnomeSearchProvider",{enumerable:!0,get:function(){return d}});let t=(e=require("dbus-next"))&&e.__esModule?e:{default:e},r="com.bluemancz.RoonPipe.SearchProvider",n=new Map,a=new Map,o=null,i="",l=null,s=null,u=null;async function c(e){if(console.log("Searching for terms:",e),!s)return[];let t=e.join(" ");return t.length<4?[]:t===i&&l?l:(o&&clearTimeout(o),i=t,l=new Promise(r=>{o=setTimeout(async()=>{if(t!==i)return void r([]);if(a.has(t))return void r(a.get(t));try{console.log("Searching for terms:",e);let o=await s(t),i=[];for(let e of o.slice(0,10)){let t=`roon_${e.item_key}`;n.set(t,e),i.push(t)}a.set(t,i),r(i)}catch(e){console.error("Search failed:",e),r([])}},300)}))}let{Interface:g}=t.default.interface;class S extends g{constructor(){super("org.gnome.Shell.SearchProvider2")}async GetInitialResultSet(e){return c(e)}async GetSubsearchResultSet(e,t){return c(t)}async GetResultMetas(e){let r=t.default.Variant,a=[];for(let t of e){let e=n.get(t);if(e){let n={id:new r("s",t),name:new r("s",e.title),description:new r("s",e.subtitle)};e.image&&(n.gicon=new r("s",e.image)),a.push(n)}}return a}async ActivateResult(e,t,r){console.log("ActivateResult called for:",e);let a=n.get(e);if(console.log("Result to activate:",a),a&&u)try{await u(a.item_key,a.sessionKey,"playNow"),console.log(`Playing: ${a.title}`)}catch(e){console.error("Failed to play track:",e)}}async LaunchSearch(e,t){console.log("LaunchSearch called - not implemented")}}async function d(e,n){s=e,u=n;try{let e=t.default.sessionBus();await e.requestName(r,0);let n=new S;e.export("/com/bluemancz/RoonPipe/SearchProvider",n),console.log("GNOME Search Provider initialized on",r)}catch(e){console.error("Failed to initialize GNOME Search Provider:",e)}}S.configureMembers({methods:{GetInitialResultSet:{inSignature:"as",outSignature:"as"},GetSubsearchResultSet:{inSignature:"asas",outSignature:"as"},GetResultMetas:{inSignature:"as",outSignature:"aa{sv}"},ActivateResult:{inSignature:"sasu",outSignature:""},LaunchSearch:{inSignature:"asu",outSignature:""}}});
|
package/dist/image-cache.js
CHANGED
|
@@ -1,103 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.getImageCachePath = getImageCachePath;
|
|
16
|
-
exports.isImageCached = isImageCached;
|
|
17
|
-
exports.cacheImage = cacheImage;
|
|
18
|
-
exports.cacheImages = cacheImages;
|
|
19
|
-
exports.clearOldCache = clearOldCache;
|
|
20
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
22
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
23
|
-
const CACHE_DIR = node_path_1.default.join(node_os_1.default.homedir(), ".cache", "roonpipe", "images");
|
|
24
|
-
// Ensure the cache directory exists
|
|
25
|
-
if (!node_fs_1.default.existsSync(CACHE_DIR)) {
|
|
26
|
-
node_fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Get the local cache path for an image
|
|
30
|
-
*/
|
|
31
|
-
function getImageCachePath(imageKey) {
|
|
32
|
-
return node_path_1.default.join(CACHE_DIR, `${imageKey}.jpg`);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Check if an image is already cached
|
|
36
|
-
*/
|
|
37
|
-
function isImageCached(imageKey) {
|
|
38
|
-
return node_fs_1.default.existsSync(getImageCachePath(imageKey));
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Download and cache an image from Roon Core
|
|
42
|
-
*/
|
|
43
|
-
function cacheImage(imageApi, imageKey) {
|
|
44
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
-
if (!imageKey) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
const cachePath = getImageCachePath(imageKey);
|
|
49
|
-
// Return cached path if already exists
|
|
50
|
-
if (isImageCached(imageKey)) {
|
|
51
|
-
return cachePath;
|
|
52
|
-
}
|
|
53
|
-
return new Promise((resolve) => {
|
|
54
|
-
imageApi.get_image(imageKey, { scale: "fit", width: 300, height: 300, format: "image/jpeg" }, (error, _contentType, imageData) => {
|
|
55
|
-
if (error || !imageData) {
|
|
56
|
-
resolve(null);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
node_fs_1.default.writeFileSync(cachePath, imageData);
|
|
61
|
-
resolve(cachePath);
|
|
62
|
-
}
|
|
63
|
-
catch (_a) {
|
|
64
|
-
resolve(null);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Cache multiple images in parallel
|
|
72
|
-
*/
|
|
73
|
-
function cacheImages(imageApi, imageKeys) {
|
|
74
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
75
|
-
const results = new Map();
|
|
76
|
-
const uniqueKeys = [...new Set(imageKeys.filter(Boolean))];
|
|
77
|
-
yield Promise.all(uniqueKeys.map((key) => __awaiter(this, void 0, void 0, function* () {
|
|
78
|
-
const path = yield cacheImage(imageApi, key);
|
|
79
|
-
results.set(key, path);
|
|
80
|
-
})));
|
|
81
|
-
return results;
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Clear old cached images (older than specified days)
|
|
86
|
-
*/
|
|
87
|
-
function clearOldCache(maxAgeDays = 30) {
|
|
88
|
-
try {
|
|
89
|
-
const files = node_fs_1.default.readdirSync(CACHE_DIR);
|
|
90
|
-
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
91
|
-
const now = Date.now();
|
|
92
|
-
for (const file of files) {
|
|
93
|
-
const filePath = node_path_1.default.join(CACHE_DIR, file);
|
|
94
|
-
const stats = node_fs_1.default.statSync(filePath);
|
|
95
|
-
if (now - stats.mtimeMs > maxAgeMs) {
|
|
96
|
-
node_fs_1.default.unlinkSync(filePath);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch (_a) {
|
|
101
|
-
// Ignore errors during cleanup
|
|
102
|
-
}
|
|
103
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=exports,t={get cacheImage(){return f},get cacheImages(){return d},get clearOldCache(){return s},get getImageCachePath(){return c},get isImageCached(){return o}};for(var r in t)Object.defineProperty(e,r,{enumerable:!0,get:Object.getOwnPropertyDescriptor(t,r).get});let n=/*#__PURE__*/u(require("node:fs")),a=/*#__PURE__*/u(require("node:os")),i=/*#__PURE__*/u(require("node:path"));function u(e){return e&&e.__esModule?e:{default:e}}let l=i.default.join(a.default.homedir(),".cache","roonpipe","images");function c(e){return i.default.join(l,`${e}.jpg`)}function o(e){return n.default.existsSync(c(e))}async function f(e,t){if(!t)return null;let r=c(t);return o(t)?r:new Promise(a=>{e.get_image(t,{scale:"fit",width:300,height:300,format:"image/jpeg"},(e,t,i)=>{if(e||!i)return void a(null);try{n.default.writeFileSync(r,i),a(r)}catch{a(null)}})})}async function d(e,t){let r=new Map,n=[...new Set(t.filter(Boolean))];return await Promise.all(n.map(async t=>{let n=await f(e,t);r.set(t,n)})),r}function s(e=30){try{let t=n.default.readdirSync(l),r=24*e*36e5,a=Date.now();for(let e of t){let t=i.default.join(l,e),u=n.default.statSync(t);a-u.mtimeMs>r&&n.default.unlinkSync(t)}}catch{}}n.default.existsSync(l)||n.default.mkdirSync(l,{recursive:!0});
|
package/dist/index.js
CHANGED
|
@@ -1,48 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
const cli_1 = require("./cli");
|
|
5
|
-
const mpris_1 = require("./mpris");
|
|
6
|
-
const roon_1 = require("./roon");
|
|
7
|
-
const socket_1 = require("./socket");
|
|
8
|
-
// Check if CLI mode is requested
|
|
9
|
-
const cliMode = process.argv.includes("--cli");
|
|
10
|
-
if (cliMode) {
|
|
11
|
-
// CLI mode - just connect to daemon via socket
|
|
12
|
-
(0, cli_1.startCLI)();
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
// Daemon mode - check for the existing instance
|
|
16
|
-
(0, socket_1.isInstanceRunning)().then((running) => {
|
|
17
|
-
if (running) {
|
|
18
|
-
console.error("❌ Another instance of RoonPipe is already running.");
|
|
19
|
-
console.error(" Stop the existing instance first, or use --cli to connect to it.");
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
// Daemon mode - start all services
|
|
23
|
-
console.log("Starting RoonPipe Daemon");
|
|
24
|
-
// Initialize MPRIS
|
|
25
|
-
(0, mpris_1.initMpris)(() => { var _a; return (_a = (0, roon_1.getCore)()) === null || _a === void 0 ? void 0 : _a.services.RoonApiTransport; }, roon_1.getZone);
|
|
26
|
-
// Initialize Roon and start socket server
|
|
27
|
-
(0, roon_1.initRoon)({
|
|
28
|
-
onCorePaired: (_core) => {
|
|
29
|
-
(0, socket_1.startSocketServer)({
|
|
30
|
-
search: roon_1.searchRoon,
|
|
31
|
-
play: roon_1.playItem,
|
|
32
|
-
});
|
|
33
|
-
},
|
|
34
|
-
onCoreUnpaired: (_core) => {
|
|
35
|
-
// Clear MPRIS metadata when unpaired
|
|
36
|
-
(0, mpris_1.updateMprisMetadata)(null, null);
|
|
37
|
-
},
|
|
38
|
-
onZoneChanged: (zone, core) => {
|
|
39
|
-
// Update MPRIS when zone changes
|
|
40
|
-
(0, mpris_1.updateMprisMetadata)(zone, core);
|
|
41
|
-
},
|
|
42
|
-
onSeekChanged: (seekPosition) => {
|
|
43
|
-
// Update MPRIS seek position
|
|
44
|
-
(0, mpris_1.updateMprisSeek)(seekPosition);
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
}
|
|
2
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});let e=require("./cli"),r=require("./gnome-search-provider"),o=require("./mpris"),n=require("./notification"),i=require("./roon"),t=require("./socket");process.argv.includes("--cli")?(0,e.startCLI)():(0,t.isInstanceRunning)().then(e=>{e&&(console.error("❌ Another instance of RoonPipe is already running."),console.error(" Stop the existing instance first, or use --cli to connect to it."),process.exit(1)),console.log("Starting RoonPipe Daemon"),(0,o.initMpris)(()=>(0,i.getCore)()?.services.RoonApiTransport,i.getZone),(0,i.initRoon)({onCorePaired:e=>{(0,t.startSocketServer)({search:i.searchRoon,play:i.playItem}),(process.env.XDG_CURRENT_DESKTOP?.includes("GNOME")||process.env.DESKTOP_SESSION?.includes("gnome"))&&(0,r.initGnomeSearchProvider)(i.searchRoon,i.playItem)},onCoreUnpaired:e=>{(0,o.updateMprisMetadata)(null,null)},onZoneChanged:(e,r)=>{(0,o.updateMprisMetadata)(e,r),(0,n.showTrackNotification)(e,r)},onSeekChanged:e=>{(0,o.updateMprisSeek)(e)}})});
|
package/dist/mpris.js
CHANGED
|
@@ -1,215 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.initMpris = initMpris;
|
|
7
|
-
exports.updateMprisMetadata = updateMprisMetadata;
|
|
8
|
-
exports.updateMprisSeek = updateMprisSeek;
|
|
9
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
-
// @ts-expect-error
|
|
11
|
-
const mpris_service_1 = __importDefault(require("mpris-service"));
|
|
12
|
-
const node_notifier_1 = __importDefault(require("node-notifier"));
|
|
13
|
-
let mpris = null;
|
|
14
|
-
let lastNotifiedTrack = null;
|
|
15
|
-
let isInitialLoad = true;
|
|
16
|
-
let lastPlaybackState = null;
|
|
17
|
-
const artworkPath = "/tmp/roon-mpris-cover.jpg";
|
|
18
|
-
const loopMap = {
|
|
19
|
-
// Roon -> MPRIS
|
|
20
|
-
loop_one: "Track",
|
|
21
|
-
loop: "Playlist",
|
|
22
|
-
disabled: "None",
|
|
23
|
-
next: "None",
|
|
24
|
-
// MPRIS -> Roon
|
|
25
|
-
Track: "loop_one",
|
|
26
|
-
Playlist: "loop",
|
|
27
|
-
None: "disabled",
|
|
28
|
-
};
|
|
29
|
-
function convertRoonLoopToMPRIS(roonLoop) {
|
|
30
|
-
return loopMap[roonLoop] || "None";
|
|
31
|
-
}
|
|
32
|
-
function convertMPRISLoopToRoon(mprisLoop) {
|
|
33
|
-
return loopMap[mprisLoop] || "disabled";
|
|
34
|
-
}
|
|
35
|
-
function initMpris(getTransport, getZone) {
|
|
36
|
-
// Helper to call Roon transport functions
|
|
37
|
-
function roonCallback(fn) {
|
|
38
|
-
const transport = getTransport();
|
|
39
|
-
const zone = getZone();
|
|
40
|
-
if (transport && zone) {
|
|
41
|
-
fn(transport, zone);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
mpris = (0, mpris_service_1.default)({
|
|
45
|
-
name: "roon",
|
|
46
|
-
identity: "Roon",
|
|
47
|
-
supportedUriSchemes: ["file"],
|
|
48
|
-
supportedMimeTypes: ["audio/mpeg", "application/ogg"],
|
|
49
|
-
supportedInterfaces: ["player"],
|
|
50
|
-
});
|
|
51
|
-
// Wire up all MPRIS events
|
|
52
|
-
const events = [
|
|
53
|
-
"play",
|
|
54
|
-
"pause",
|
|
55
|
-
"stop",
|
|
56
|
-
"playpause",
|
|
57
|
-
"next",
|
|
58
|
-
"previous",
|
|
59
|
-
"seek",
|
|
60
|
-
"position",
|
|
61
|
-
"volume",
|
|
62
|
-
"loopStatus",
|
|
63
|
-
"shuffle",
|
|
64
|
-
"open",
|
|
65
|
-
];
|
|
66
|
-
events.forEach((event) => {
|
|
67
|
-
mpris.on(event, (data) => {
|
|
68
|
-
switch (event) {
|
|
69
|
-
case "seek":
|
|
70
|
-
roonCallback((t, z) => t.seek(z, "relative", data / 1000000));
|
|
71
|
-
break;
|
|
72
|
-
case "position":
|
|
73
|
-
roonCallback((t, z) => t.seek(z, "absolute", data.position / 1000000));
|
|
74
|
-
break;
|
|
75
|
-
case "volume":
|
|
76
|
-
roonCallback((t, z) => {
|
|
77
|
-
var _a, _b;
|
|
78
|
-
if (!((_b = (_a = z.outputs) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.volume))
|
|
79
|
-
return;
|
|
80
|
-
const output = z.outputs[0];
|
|
81
|
-
const vol = output.volume;
|
|
82
|
-
const roonVolume = vol.min + data * (vol.max - vol.min);
|
|
83
|
-
t.change_volume(output, "absolute", Math.round(roonVolume));
|
|
84
|
-
});
|
|
85
|
-
break;
|
|
86
|
-
case "loopStatus":
|
|
87
|
-
roonCallback((t, z) => t.change_settings(z, { loop: convertMPRISLoopToRoon(data) }));
|
|
88
|
-
break;
|
|
89
|
-
case "shuffle":
|
|
90
|
-
roonCallback((t, z) => t.change_settings(z, { shuffle: data }));
|
|
91
|
-
break;
|
|
92
|
-
case "open":
|
|
93
|
-
console.log("MPRIS command: open", data.uri);
|
|
94
|
-
break;
|
|
95
|
-
default:
|
|
96
|
-
roonCallback((t, z) => t.control(z, event));
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
// Position getter - dynamically reads from zone
|
|
102
|
-
mpris.getPosition = () => {
|
|
103
|
-
var _a;
|
|
104
|
-
const zone = getZone();
|
|
105
|
-
return ((_a = zone === null || zone === void 0 ? void 0 : zone.now_playing) === null || _a === void 0 ? void 0 : _a.seek_position) ? zone.now_playing.seek_position * 1000000 : 0;
|
|
106
|
-
};
|
|
107
|
-
// Set initial states
|
|
108
|
-
mpris.canControl = true;
|
|
109
|
-
mpris.canPlay = true;
|
|
110
|
-
mpris.canPause = true;
|
|
111
|
-
mpris.canGoNext = true;
|
|
112
|
-
mpris.canGoPrevious = true;
|
|
113
|
-
mpris.canSeek = true;
|
|
114
|
-
mpris.playbackStatus = "Stopped";
|
|
115
|
-
console.log("MPRIS player initialized");
|
|
116
|
-
}
|
|
117
|
-
function fetchArtwork(core, imageKey, callback) {
|
|
118
|
-
const image = core.services.RoonApiImage;
|
|
119
|
-
image.get_image(imageKey, { scale: "fit", width: 256, height: 256, format: "image/jpeg" }, (error, _contentType, imageData) => {
|
|
120
|
-
if (error) {
|
|
121
|
-
console.error("Failed to fetch artwork:", error);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
node_fs_1.default.writeFileSync(artworkPath, imageData);
|
|
125
|
-
console.log("Artwork saved to", artworkPath);
|
|
126
|
-
callback();
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
function showTrackNotification(zone) {
|
|
130
|
-
var _a, _b, _c;
|
|
131
|
-
if (!zone || !zone.now_playing)
|
|
132
|
-
return;
|
|
133
|
-
const np = zone.now_playing;
|
|
134
|
-
const trackId = np.image_key || "";
|
|
135
|
-
const currentState = zone.state;
|
|
136
|
-
// Skip notification on an initial load
|
|
137
|
-
if (isInitialLoad) {
|
|
138
|
-
lastNotifiedTrack = trackId;
|
|
139
|
-
lastPlaybackState = currentState;
|
|
140
|
-
isInitialLoad = false;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// Show notification if the track changed or started playing
|
|
144
|
-
const trackChanged = trackId !== lastNotifiedTrack;
|
|
145
|
-
const startedPlaying = currentState === "playing" && lastPlaybackState !== "playing";
|
|
146
|
-
if (trackChanged || startedPlaying) {
|
|
147
|
-
lastNotifiedTrack = trackId;
|
|
148
|
-
node_notifier_1.default.notify({
|
|
149
|
-
title: ((_a = np.three_line) === null || _a === void 0 ? void 0 : _a.line1) || "Unknown Track",
|
|
150
|
-
message: `${((_b = np.three_line) === null || _b === void 0 ? void 0 : _b.line2) || "Unknown Artist"}\n${((_c = np.three_line) === null || _c === void 0 ? void 0 : _c.line3) || ""}`,
|
|
151
|
-
timeout: 5,
|
|
152
|
-
icon: artworkPath,
|
|
153
|
-
hint: "string:x-canonical-private-synchronous:roonpipe",
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
lastPlaybackState = currentState;
|
|
157
|
-
}
|
|
158
|
-
function updateVolume(zone) {
|
|
159
|
-
if (!mpris)
|
|
160
|
-
return;
|
|
161
|
-
if (!zone || !zone.outputs || zone.outputs.length === 0) {
|
|
162
|
-
mpris.volume = 0;
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const output = zone.outputs[0];
|
|
166
|
-
if (!output.volume) {
|
|
167
|
-
mpris.volume = 0;
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const vol = output.volume;
|
|
171
|
-
mpris.volume = (vol.value - vol.min) / (vol.max - vol.min);
|
|
172
|
-
}
|
|
173
|
-
function updateMprisMetadata(zone, core) {
|
|
174
|
-
var _a, _b, _c;
|
|
175
|
-
if (!mpris)
|
|
176
|
-
return;
|
|
177
|
-
if (!zone || !zone.now_playing) {
|
|
178
|
-
mpris.metadata = {};
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const np = zone.now_playing;
|
|
182
|
-
// Fetch artwork if image_key exists
|
|
183
|
-
if (np.image_key) {
|
|
184
|
-
fetchArtwork(core, np.image_key, () => {
|
|
185
|
-
showTrackNotification(zone);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
mpris.metadata = {
|
|
189
|
-
"mpris:trackid": mpris.objectPath(`track/${np.image_key || "0"}`),
|
|
190
|
-
"xesam:title": ((_a = np.three_line) === null || _a === void 0 ? void 0 : _a.line1) || "Unknown",
|
|
191
|
-
"xesam:artist": ((_b = np.three_line) === null || _b === void 0 ? void 0 : _b.line2) ? [np.three_line.line2] : [],
|
|
192
|
-
"xesam:album": ((_c = np.three_line) === null || _c === void 0 ? void 0 : _c.line3) || "",
|
|
193
|
-
"mpris:length": (np.length || 0) * 1000000,
|
|
194
|
-
"mpris:artUrl": `file://${artworkPath}`,
|
|
195
|
-
};
|
|
196
|
-
// For playpause to work, we need canPlay OR canPause to be true
|
|
197
|
-
// When playing: is_pause_allowed=true, is_play_allowed=false
|
|
198
|
-
// When paused: is_pause_allowed=false, is_play_allowed=true
|
|
199
|
-
// We set both to true if either action is allowed, so playpause always works
|
|
200
|
-
const canTogglePlayback = zone.is_play_allowed || zone.is_pause_allowed;
|
|
201
|
-
mpris.canPlay = canTogglePlayback;
|
|
202
|
-
mpris.canPause = canTogglePlayback;
|
|
203
|
-
mpris.canGoNext = zone.is_next_allowed;
|
|
204
|
-
mpris.canGoPrevious = zone.is_previous_allowed;
|
|
205
|
-
mpris.playbackStatus = zone.state === "playing" ? "Playing" : "Paused";
|
|
206
|
-
mpris.loopStatus = convertRoonLoopToMPRIS(zone.settings.loop);
|
|
207
|
-
mpris.shuffle = zone.settings.shuffle;
|
|
208
|
-
mpris.canSeek = zone.is_seek_allowed;
|
|
209
|
-
updateVolume(zone);
|
|
210
|
-
}
|
|
211
|
-
function updateMprisSeek(seekPosition) {
|
|
212
|
-
if (!mpris)
|
|
213
|
-
return;
|
|
214
|
-
mpris.seeked(seekPosition);
|
|
215
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=exports,a={get initMpris(){return u},get updateMprisMetadata(){return p},get updateMprisSeek(){return c}};for(var o in a)Object.defineProperty(t,o,{enumerable:!0,get:Object.getOwnPropertyDescriptor(a,o).get});let s=(e=require("mpris-service"))&&e.__esModule?e:{default:e},i=require("./image-cache"),n=require("./roon"),l=null,r={loop_one:"Track",loop:"Playlist",disabled:"None",next:"None",Track:"loop_one",Playlist:"loop",None:"disabled"};function u(e,t){function a(a){let o=e(),s=t();o&&s&&a(o,s)}l=(0,s.default)({name:"roon",identity:"Roon",supportedUriSchemes:["file"],supportedMimeTypes:["audio/mpeg","application/ogg"],supportedInterfaces:["player"]}),["play","pause","stop","playpause","next","previous","seek","position","volume","loopStatus","shuffle","open"].forEach(e=>{l.on(e,t=>{switch(e){case"seek":a((e,a)=>e.seek(a,"relative",t/1e6));break;case"position":a((e,a)=>e.seek(a,"absolute",t.position/1e6));break;case"volume":a((e,a)=>{if(!a.outputs?.[0]?.volume)return;let o=a.outputs[0],s=o.volume,i=s.min+t*(s.max-s.min);e.change_volume(o,"absolute",Math.round(i))});break;case"loopStatus":a((e,a)=>e.change_settings(a,{loop:r[t]||"disabled"}));break;case"shuffle":a((e,a)=>e.change_settings(a,{shuffle:t}));break;case"open":console.log("MPRIS command: open",t.uri);break;default:a((t,a)=>t.control(a,e))}})}),l.getPosition=()=>{let e=t();return e?.now_playing?.seek_position?1e6*e.now_playing.seek_position:0},l.canControl=!0,l.canPlay=!0,l.canPause=!0,l.canGoNext=!0,l.canGoPrevious=!0,l.canSeek=!0,l.playbackStatus="Stopped",console.log("MPRIS player initialized")}async function p(e,t){if(!l)return;if(!e||!e.now_playing){l.metadata={},l.playbackStatus="Stopped";return}let a=e.now_playing,o=(0,n.parseNowPlaying)(a),s=null;a.image_key&&t?.services?.RoonApiImage&&(s=await (0,i.cacheImage)(t.services.RoonApiImage,a.image_key)),l.metadata={"mpris:trackid":l.objectPath(`track/${a.image_key||"0"}`),"xesam:title":o.title,"xesam:artist":o.artists,"xesam:album":o.album,"mpris:length":1e6*(a.length||0),...s&&{"mpris:artUrl":`file://${s}`}};let u=e.is_play_allowed||e.is_pause_allowed;l.canPlay=u,l.canPause=u,l.canGoNext=e.is_next_allowed,l.canGoPrevious=e.is_previous_allowed,l.playbackStatus="playing"===e.state?"Playing":"Paused",l.loopStatus=r[e.settings.loop]||"None",l.shuffle=e.settings.shuffle,l.canSeek=e.is_seek_allowed,function(e){if(!l)return;if(!e||!e.outputs||0===e.outputs.length){l.volume=0;return}let t=e.outputs[0];if(!t.volume){l.volume=0;return}let a=t.volume;l.volume=(a.value-a.min)/(a.max-a.min)}(e)}function c(e){l&&l.seeked(e)}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"showTrackNotification",{enumerable:!0,get:function(){return l}});let e=require("node:child_process"),i=require("./image-cache"),n=require("./roon"),t=null,o=null,r=!0,a=null;async function l(l,c){if(!l?.now_playing)return;let s=l.now_playing,u=s.image_key||"",g=l.state;if(r){r=!1,t=u,a=g;return}let p=u!==t,m="playing"===g&&"playing"!==a;a=g,(p||m)&&(o&&clearTimeout(o),o=setTimeout(async()=>{if(u===t){o=null;return}t=u;let r=(0,n.parseNowPlaying)(s),a=null;s.image_key&&c?.services?.RoonApiImage&&(a=await (0,i.cacheImage)(c.services.RoonApiImage,s.image_key)),console.log("Showing notification for track:",r.title);let l=["--app-name=Roon","--icon=audio-x-generic","--hint=int:transient:1",...a?[`--hint=string:image-path:${a}`]:[],"--expire-time=5000",r.title,`${r.artists.join(", ")}
|
|
2
|
+
${r.album}`];try{(0,e.execSync)(`notify-send ${l.map(e=>`'${e.replace(/'/g,"'\\''")}'`).join(" ")}`)}catch(e){console.error("Failed to show notification:",e)}o=null},150))}
|
package/dist/roon.js
CHANGED
|
@@ -1,265 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.initRoon = initRoon;
|
|
16
|
-
exports.searchRoon = searchRoon;
|
|
17
|
-
exports.playItem = playItem;
|
|
18
|
-
exports.getZone = getZone;
|
|
19
|
-
exports.getCore = getCore;
|
|
20
|
-
// @ts-expect-error
|
|
21
|
-
const node_roon_api_1 = __importDefault(require("node-roon-api"));
|
|
22
|
-
// @ts-expect-error
|
|
23
|
-
const node_roon_api_browse_1 = __importDefault(require("node-roon-api-browse"));
|
|
24
|
-
// @ts-expect-error
|
|
25
|
-
const node_roon_api_image_1 = __importDefault(require("node-roon-api-image"));
|
|
26
|
-
// @ts-expect-error
|
|
27
|
-
const node_roon_api_transport_1 = __importDefault(require("node-roon-api-transport"));
|
|
28
|
-
const image_cache_1 = require("./image-cache");
|
|
29
|
-
let zone = null;
|
|
30
|
-
let coreInstance = null;
|
|
31
|
-
function initRoon(callbacks) {
|
|
32
|
-
const roon = new node_roon_api_1.default({
|
|
33
|
-
extension_id: "com.bluemancz.roonpipe",
|
|
34
|
-
display_name: "RoonPipe",
|
|
35
|
-
display_version: "1.0.2",
|
|
36
|
-
publisher: "BlueManCZ",
|
|
37
|
-
email: "your@email.com",
|
|
38
|
-
website: "https://github.com/bluemancz/roonpipe",
|
|
39
|
-
core_paired: (core) => {
|
|
40
|
-
coreInstance = core;
|
|
41
|
-
const transport = core.services.RoonApiTransport;
|
|
42
|
-
transport.subscribe_zones((cmd, data) => {
|
|
43
|
-
if (cmd === "Subscribed") {
|
|
44
|
-
zone = data.zones.find((z) => z.state === "playing") || data.zones[0];
|
|
45
|
-
callbacks.onZoneChanged(zone, core);
|
|
46
|
-
}
|
|
47
|
-
else if (cmd === "Changed") {
|
|
48
|
-
if (data.zones_changed) {
|
|
49
|
-
const playingZone = data.zones_changed.find((z) => z.state === "playing");
|
|
50
|
-
if (playingZone) {
|
|
51
|
-
zone = playingZone;
|
|
52
|
-
}
|
|
53
|
-
else if (zone) {
|
|
54
|
-
zone =
|
|
55
|
-
data.zones_changed.find((z) => z.zone_id === zone.zone_id) ||
|
|
56
|
-
zone;
|
|
57
|
-
}
|
|
58
|
-
callbacks.onZoneChanged(zone, core);
|
|
59
|
-
}
|
|
60
|
-
if (data.zones_seek_changed) {
|
|
61
|
-
const seekUpdate = data.zones_seek_changed.find((z) => z.zone_id === (zone === null || zone === void 0 ? void 0 : zone.zone_id));
|
|
62
|
-
if (seekUpdate && zone) {
|
|
63
|
-
zone.now_playing.seek_position = seekUpdate.seek_position;
|
|
64
|
-
callbacks.onSeekChanged(seekUpdate.seek_position * 1000000);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
callbacks.onCorePaired(core);
|
|
70
|
-
console.log(`Core paired: ${core.display_name}`);
|
|
71
|
-
},
|
|
72
|
-
core_unpaired: (core) => {
|
|
73
|
-
zone = null;
|
|
74
|
-
coreInstance = null;
|
|
75
|
-
callbacks.onCoreUnpaired(core);
|
|
76
|
-
console.log(`Core unpaired: ${core.display_name}`);
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
roon.init_services({
|
|
80
|
-
required_services: [node_roon_api_browse_1.default, node_roon_api_image_1.default, node_roon_api_transport_1.default],
|
|
81
|
-
});
|
|
82
|
-
roon.start_discovery();
|
|
83
|
-
}
|
|
84
|
-
function searchRoon(query) {
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
if (!coreInstance) {
|
|
87
|
-
reject("Roon Core not connected");
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
if (!zone) {
|
|
91
|
-
reject("No active zone");
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const browse = coreInstance.services.RoonApiBrowse;
|
|
95
|
-
const sessionKey = `search_${Date.now()}`;
|
|
96
|
-
const opts = {
|
|
97
|
-
hierarchy: "search",
|
|
98
|
-
input: query,
|
|
99
|
-
multi_session_key: sessionKey,
|
|
100
|
-
zone_or_output_id: zone.zone_id,
|
|
101
|
-
};
|
|
102
|
-
browse.browse(opts, (error, result) => {
|
|
103
|
-
if (error) {
|
|
104
|
-
reject(error);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
browse.load(Object.assign(Object.assign({}, opts), { offset: 0, count: result.list.count }), (loadError, loadResult) => {
|
|
108
|
-
var _a;
|
|
109
|
-
if (loadError) {
|
|
110
|
-
reject(loadError);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const tracksCategory = (_a = loadResult.items) === null || _a === void 0 ? void 0 : _a.find((item) => item.title === "Tracks");
|
|
114
|
-
if (!tracksCategory) {
|
|
115
|
-
// No tracks category found - return an empty array
|
|
116
|
-
resolve([]);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
browse.browse(Object.assign(Object.assign({}, opts), { item_key: tracksCategory.item_key }), (browseError, browseResult) => {
|
|
120
|
-
if (browseError) {
|
|
121
|
-
reject(browseError);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
browse.load(Object.assign(Object.assign({}, opts), { item_key: tracksCategory.item_key, offset: 0, count: Math.min(browseResult.list.count, 50) }), (tracksError, tracksResult) => __awaiter(this, void 0, void 0, function* () {
|
|
125
|
-
var _a, _b;
|
|
126
|
-
if (tracksError) {
|
|
127
|
-
reject(tracksError);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
// Cache all images in parallel
|
|
131
|
-
const imageKeys = ((_a = tracksResult.items) === null || _a === void 0 ? void 0 : _a.map((item) => item.image_key).filter(Boolean)) || [];
|
|
132
|
-
const imageApi = coreInstance.services.RoonApiImage;
|
|
133
|
-
const cachedImages = yield (0, image_cache_1.cacheImages)(imageApi, imageKeys);
|
|
134
|
-
const items = ((_b = tracksResult.items) === null || _b === void 0 ? void 0 : _b.map((item) => ({
|
|
135
|
-
title: item.title || "Unknown",
|
|
136
|
-
subtitle: item.subtitle || "",
|
|
137
|
-
item_key: item.item_key,
|
|
138
|
-
image_key: item.image_key,
|
|
139
|
-
image: cachedImages.get(item.image_key) || null,
|
|
140
|
-
hint: item.hint,
|
|
141
|
-
sessionKey: sessionKey,
|
|
142
|
-
}))) || [];
|
|
143
|
-
resolve(items);
|
|
144
|
-
}));
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
const ACTION_TITLES = {
|
|
151
|
-
play: ["Play Now", "Play"],
|
|
152
|
-
queue: ["Queue", "Add to Queue"],
|
|
153
|
-
addNext: ["Play From Here", "Add Next"],
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* Skip to the next track in the queue
|
|
157
|
-
*/
|
|
158
|
-
function skipToNext() {
|
|
159
|
-
return new Promise((resolve, reject) => {
|
|
160
|
-
if (!coreInstance || !zone) {
|
|
161
|
-
reject("Not connected");
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
coreInstance.services.RoonApiTransport.control(zone, "next", (err) => {
|
|
165
|
-
if (err)
|
|
166
|
-
reject(err);
|
|
167
|
-
else
|
|
168
|
-
resolve();
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
function playItem(itemKey_1, sessionKey_1) {
|
|
173
|
-
return __awaiter(this, arguments, void 0, function* (itemKey, sessionKey, action = "play") {
|
|
174
|
-
// "playNow" = add next + skip to it (preserves queue)
|
|
175
|
-
if (action === "playNow") {
|
|
176
|
-
yield playItemInternal(itemKey, sessionKey, "addNext");
|
|
177
|
-
yield skipToNext();
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
return playItemInternal(itemKey, sessionKey, action);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
function playItemInternal(itemKey, sessionKey, action) {
|
|
184
|
-
return new Promise((resolve, reject) => {
|
|
185
|
-
if (!coreInstance) {
|
|
186
|
-
reject("Roon Core not connected");
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (!zone) {
|
|
190
|
-
reject("No active zone");
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
const browse = coreInstance.services.RoonApiBrowse;
|
|
194
|
-
const actionTitles = ACTION_TITLES[action];
|
|
195
|
-
function loadUntilAction(currentItemKey, depth = 0) {
|
|
196
|
-
if (depth > 5) {
|
|
197
|
-
reject("Too many levels, cannot find action");
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
browse.browse({
|
|
201
|
-
hierarchy: "search",
|
|
202
|
-
multi_session_key: sessionKey,
|
|
203
|
-
item_key: currentItemKey,
|
|
204
|
-
zone_or_output_id: zone.zone_id,
|
|
205
|
-
}, (browseError, browseResult) => {
|
|
206
|
-
var _a;
|
|
207
|
-
if (browseError) {
|
|
208
|
-
reject(browseError);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
browse.load({
|
|
212
|
-
hierarchy: "search",
|
|
213
|
-
multi_session_key: sessionKey,
|
|
214
|
-
item_key: currentItemKey,
|
|
215
|
-
offset: 0,
|
|
216
|
-
count: ((_a = browseResult.list) === null || _a === void 0 ? void 0 : _a.count) || 10,
|
|
217
|
-
zone_or_output_id: zone.zone_id,
|
|
218
|
-
}, (loadError, loadResult) => {
|
|
219
|
-
if (loadError) {
|
|
220
|
-
reject(loadError);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (!loadResult.items || loadResult.items.length === 0) {
|
|
224
|
-
reject("No items found");
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const targetAction = loadResult.items.find((item) => item.hint === "action" &&
|
|
228
|
-
actionTitles.some((title) => item.title === title));
|
|
229
|
-
if (targetAction) {
|
|
230
|
-
browse.browse({
|
|
231
|
-
hierarchy: "search",
|
|
232
|
-
multi_session_key: sessionKey,
|
|
233
|
-
item_key: targetAction.item_key,
|
|
234
|
-
zone_or_output_id: zone.zone_id,
|
|
235
|
-
}, (playError) => {
|
|
236
|
-
if (playError) {
|
|
237
|
-
reject(playError);
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
console.log(`Successfully executed action: ${action}`);
|
|
241
|
-
resolve();
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
const actionList = loadResult.items.find((item) => item.hint === "action_list");
|
|
247
|
-
if (actionList) {
|
|
248
|
-
loadUntilAction(actionList.item_key, depth + 1);
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
reject(`Could not find ${action} action or next level`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
loadUntilAction(itemKey);
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
function getZone() {
|
|
261
|
-
return zone;
|
|
262
|
-
}
|
|
263
|
-
function getCore() {
|
|
264
|
-
return coreInstance;
|
|
265
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=exports,n={get getCore(){return h},get getZone(){return g},get initRoon(){return c},get parseNowPlaying(){return d},get playItem(){return y},get searchRoon(){return p}};for(var i in n)Object.defineProperty(e,i,{enumerable:!0,get:Object.getOwnPropertyDescriptor(n,i).get});let o=/*#__PURE__*/l(require("node-roon-api")),t=/*#__PURE__*/l(require("node-roon-api-browse")),r=/*#__PURE__*/l(require("node-roon-api-image")),s=/*#__PURE__*/l(require("node-roon-api-transport")),a=require("./image-cache");function l(e){return e&&e.__esModule?e:{default:e}}let u=null,_=null,d=e=>{let n=e.three_line?.line1||"Unknown Track";return{title:n,artists:e.three_line?.line2?[e.three_line.line2.split(" / ").map(e=>e.trim())[0]]:["Unknown Artist"],album:e.three_line?.line3||""}};function c(e){let n=new o.default({extension_id:"com.bluemancz.roonpipe",display_name:"RoonPipe",display_version:"1.0.4",publisher:"BlueManCZ",email:"your@email.com",website:"https://github.com/bluemancz/roonpipe",log_level:"none",core_paired:n=>{_=n,n.services.RoonApiTransport.subscribe_zones((i,o)=>{if("Subscribed"===i)u=o.zones.find(e=>"playing"===e.state)||o.zones[0],e.onZoneChanged(u,n);else if("Changed"===i){if(o.zones_changed){let i=o.zones_changed.find(e=>"playing"===e.state);i?u=i:u&&(u=o.zones_changed.find(e=>e.zone_id===u.zone_id)||u),e.onZoneChanged(u,n)}if(o.zones_seek_changed){let n=o.zones_seek_changed.find(e=>e.zone_id===u?.zone_id);n&&u?.now_playing&&(u.now_playing.seek_position=n.seek_position,e.onSeekChanged(1e6*n.seek_position))}}}),e.onCorePaired(n),console.log(`Core paired: ${n.display_name}`)},core_unpaired:n=>{u=null,_=null,e.onCoreUnpaired(n),console.log(`Core unpaired: ${n.display_name}`)}});n.init_services({required_services:[t.default,r.default,s.default]}),n.start_discovery()}function p(e){return new Promise((n,i)=>{if(!_)return void i("Roon Core not connected");if(!u)return void i("No active zone");let o=_.services.RoonApiBrowse,t=`search_${Date.now()}`,r={hierarchy:"search",input:e,multi_session_key:t,zone_or_output_id:u.zone_id};o.browse(r,(e,s)=>{e?i(e):o.load({...r,offset:0,count:s.list.count},(e,s)=>{if(e)return void i(e);let l=s.items?.find(e=>"Tracks"===e.title);l?o.browse({...r,item_key:l.item_key},(e,s)=>{e?i(e):o.load({...r,item_key:l.item_key,offset:0,count:Math.min(s.list.count,50)},async(e,o)=>{if(e)return void i(e);let r=o.items?.map(e=>e.image_key).filter(Boolean)||[],s=_.services.RoonApiImage,l=await (0,a.cacheImages)(s,r);n(o.items?.map(e=>({title:e.title||"Unknown Track",subtitle:e.subtitle?e.subtitle.split(", ")[0]:"Unknown Artist",item_key:e.item_key,image:l.get(e.image_key)||null,hint:e.hint,sessionKey:t}))||[])})}):n([])})})})}let m={play:["Play Now","Play"],queue:["Queue","Add to Queue"],addNext:["Play From Here","Add Next"]};async function y(e,n,i="play"){return"playNow"===i?void(u&&u.now_playing&&("playing"===u.state||"paused"===u.state)?(await f(e,n,"addNext"),await new Promise((e,n)=>{_&&u?_.services.RoonApiTransport.control(u,"next",i=>{i?n(i):e()}):n("Not connected")})):await f(e,n,"play")):f(e,n,i)}function f(e,n,i){return new Promise((o,t)=>{if(!_)return void t("Roon Core not connected");if(!u)return void t("No active zone");let r=_.services.RoonApiBrowse,s=m[i];!function e(a,l=0){l>5?t("Too many levels, cannot find action"):r.browse({hierarchy:"search",multi_session_key:n,item_key:a,zone_or_output_id:u.zone_id},(_,d)=>{_?t(_):r.load({hierarchy:"search",multi_session_key:n,item_key:a,offset:0,count:d.list?.count||10,zone_or_output_id:u.zone_id},(a,_)=>{if(a)return void t(a);if(!_.items||0===_.items.length)return void t("No items found");let d=_.items.find(e=>"action"===e.hint&&s.some(n=>e.title===n));if(d)r.browse({hierarchy:"search",multi_session_key:n,item_key:d.item_key,zone_or_output_id:u.zone_id},e=>{e?t(e):(console.log(`Successfully executed action: ${i}`),o())});else{let n=_.items.find(e=>"action_list"===e.hint);n?e(n.item_key,l+1):t(`Could not find ${i} action or next level`)}})})}(e)})}function g(){return u}function h(){return _}
|
package/dist/socket.js
CHANGED
|
@@ -1,97 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.isInstanceRunning = isInstanceRunning;
|
|
16
|
-
exports.startSocketServer = startSocketServer;
|
|
17
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
|
-
const node_net_1 = __importDefault(require("node:net"));
|
|
19
|
-
const SOCKET_PATH = "/tmp/roonpipe.sock";
|
|
20
|
-
let socketServer = null;
|
|
21
|
-
/**
|
|
22
|
-
* Check if another instance is already running by trying to connect to the socket
|
|
23
|
-
*/
|
|
24
|
-
function isInstanceRunning() {
|
|
25
|
-
return new Promise((resolve) => {
|
|
26
|
-
if (!node_fs_1.default.existsSync(SOCKET_PATH)) {
|
|
27
|
-
resolve(false);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const client = node_net_1.default.createConnection({ path: SOCKET_PATH }, () => {
|
|
31
|
-
// Connection successful - another instance is running
|
|
32
|
-
client.end();
|
|
33
|
-
resolve(true);
|
|
34
|
-
});
|
|
35
|
-
client.on("error", () => {
|
|
36
|
-
// Connection failed - socket is stale, no instance running
|
|
37
|
-
resolve(false);
|
|
38
|
-
});
|
|
39
|
-
// Timeout after 1 second
|
|
40
|
-
client.setTimeout(1000, () => {
|
|
41
|
-
client.destroy();
|
|
42
|
-
resolve(false);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
function startSocketServer(handlers) {
|
|
47
|
-
// Remove old socket if exists
|
|
48
|
-
if (node_fs_1.default.existsSync(SOCKET_PATH)) {
|
|
49
|
-
node_fs_1.default.unlinkSync(SOCKET_PATH);
|
|
50
|
-
}
|
|
51
|
-
socketServer = node_net_1.default.createServer((client) => {
|
|
52
|
-
console.log("Client connected to socket");
|
|
53
|
-
client.on("data", (data) => __awaiter(this, void 0, void 0, function* () {
|
|
54
|
-
try {
|
|
55
|
-
const request = JSON.parse(data.toString());
|
|
56
|
-
console.log("Received request:", request);
|
|
57
|
-
if (request.command === "search") {
|
|
58
|
-
try {
|
|
59
|
-
const results = yield handlers.search(request.query);
|
|
60
|
-
client.write(`${JSON.stringify({ error: null, results })}\n`);
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
client.write(`${JSON.stringify({ error: String(error), results: null })}\n`);
|
|
64
|
-
}
|
|
65
|
-
client.end();
|
|
66
|
-
}
|
|
67
|
-
else if (request.command === "play") {
|
|
68
|
-
try {
|
|
69
|
-
yield handlers.play(request.item_key, request.session_key, request.action);
|
|
70
|
-
client.write(`${JSON.stringify({ error: null, success: true })}\n`);
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
client.write(`${JSON.stringify({ error: String(error), success: false })}\n`);
|
|
74
|
-
}
|
|
75
|
-
client.end();
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
client.write(`${JSON.stringify({ error: "Unknown command" })}\n`);
|
|
79
|
-
client.end();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
console.error("Socket error:", e);
|
|
84
|
-
client.write(`${JSON.stringify({ error: "Invalid request format" })}\n`);
|
|
85
|
-
client.end();
|
|
86
|
-
}
|
|
87
|
-
}));
|
|
88
|
-
client.on("error", (err) => {
|
|
89
|
-
console.error("Client error:", err);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
socketServer.listen(SOCKET_PATH, () => {
|
|
93
|
-
console.log("Unix socket server listening on", SOCKET_PATH);
|
|
94
|
-
// Set permissions so any user can connect
|
|
95
|
-
node_fs_1.default.chmodSync(SOCKET_PATH, 0o666);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=exports,r={get isInstanceRunning(){return c},get startSocketServer(){return l}};for(var t in r)Object.defineProperty(e,t,{enumerable:!0,get:Object.getOwnPropertyDescriptor(r,t).get});let n=/*#__PURE__*/i(require("node:fs")),o=/*#__PURE__*/i(require("node:net"));function i(e){return e&&e.__esModule?e:{default:e}}let s="/tmp/roonpipe.sock";function c(){return new Promise(e=>{if(!n.default.existsSync(s))return void e(!1);let r=o.default.createConnection({path:s},()=>{r.end(),e(!0)});r.on("error",()=>{e(!1)}),r.setTimeout(1e3,()=>{r.destroy(),e(!1)})})}function l(e){n.default.existsSync(s)&&n.default.unlinkSync(s),o.default.createServer(r=>{console.log("Client connected to socket"),r.on("data",async t=>{try{let n=JSON.parse(t.toString());if(console.log("Received request:",n),"search"===n.command){try{let t=await e.search(n.query);r.write(`${JSON.stringify({error:null,results:t})}
|
|
2
|
+
`)}catch(e){r.write(`${JSON.stringify({error:String(e),results:null})}
|
|
3
|
+
`)}r.end()}else if("play"===n.command){try{await e.play(n.item_key,n.session_key,n.action),r.write(`${JSON.stringify({error:null,success:!0})}
|
|
4
|
+
`)}catch(e){r.write(`${JSON.stringify({error:String(e),success:!1})}
|
|
5
|
+
`)}r.end()}else r.write(`${JSON.stringify({error:"Unknown command"})}
|
|
6
|
+
`),r.end()}catch(e){console.error("Socket error:",e),r.write(`${JSON.stringify({error:"Invalid request format"})}
|
|
7
|
+
`),r.end()}}),r.on("error",e=>{console.error("Client error:",e)})}).listen(s,()=>{console.log("Unix socket server listening on",s),n.default.chmodSync(s,438)})}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roonpipe",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Linux integration for Roon – MPRIS support, media keys, desktop notifications, and interactive search CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,28 +22,35 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@biomejs/biome": "^2.3.11",
|
|
25
|
-
"@
|
|
25
|
+
"@swc/cli": "^0.7.10",
|
|
26
|
+
"@swc/core": "^1.15.10",
|
|
27
|
+
"@swc/register": "^0.1.10",
|
|
28
|
+
"@types/node": "^25.0.9",
|
|
26
29
|
"nodemon": "^3.1.11",
|
|
27
|
-
"ts-node": "^10.9.2",
|
|
28
30
|
"typescript": "^5.9.3"
|
|
29
31
|
},
|
|
30
32
|
"files": [
|
|
31
|
-
"dist"
|
|
33
|
+
"dist",
|
|
34
|
+
"scripts",
|
|
35
|
+
"data",
|
|
36
|
+
"!scripts/postbuild.ts"
|
|
32
37
|
],
|
|
33
38
|
"dependencies": {
|
|
34
39
|
"@inquirer/prompts": "^8.2.0",
|
|
40
|
+
"dbus-next": "^0.10.2",
|
|
35
41
|
"mpris-service": "^2.1.2",
|
|
36
|
-
"node-notifier": "^10.0.1",
|
|
37
42
|
"node-roon-api": "github:roonlabs/node-roon-api",
|
|
38
43
|
"node-roon-api-browse": "github:roonlabs/node-roon-api-browse",
|
|
39
44
|
"node-roon-api-image": "github:roonlabs/node-roon-api-image",
|
|
40
45
|
"node-roon-api-transport": "github:roonlabs/node-roon-api-transport"
|
|
41
46
|
},
|
|
42
47
|
"scripts": {
|
|
43
|
-
"build": "
|
|
48
|
+
"build": "swc src -d dist --strip-leading-paths",
|
|
49
|
+
"postbuild": "node --require @swc/register scripts/postbuild.ts",
|
|
50
|
+
"postinstall": "bash scripts/install-gnome-search-provider.sh",
|
|
44
51
|
"start": "node dist/index.js",
|
|
45
52
|
"dev": "nodemon src/index.ts",
|
|
46
|
-
"cli": "
|
|
53
|
+
"cli": "pnpm build && node dist/index.js --cli",
|
|
47
54
|
"biome": "biome check"
|
|
48
55
|
}
|
|
49
56
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Install GNOME Shell Search Provider for RoonPipe
|
|
4
|
+
|
|
5
|
+
# Check if running as root
|
|
6
|
+
if [ "$EUID" -ne 0 ]; then
|
|
7
|
+
echo "This script must be run as root to install system-wide."
|
|
8
|
+
echo "Try: sudo $0"
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
DESKTOP_FILE="com.bluemancz.RoonPipe.desktop"
|
|
13
|
+
SEARCH_PROVIDER_FILE="com.bluemancz.RoonPipe.SearchProvider.ini"
|
|
14
|
+
SERVICE_FILE="com.bluemancz.RoonPipe.SearchProvider.service"
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
DATA_DIR="$SCRIPT_DIR/../data"
|
|
18
|
+
|
|
19
|
+
# Install desktop file system-wide
|
|
20
|
+
cp "$DATA_DIR/$DESKTOP_FILE" "/usr/share/applications/"
|
|
21
|
+
echo "Installed: /usr/share/applications/$DESKTOP_FILE"
|
|
22
|
+
|
|
23
|
+
# Install search provider system-wide
|
|
24
|
+
mkdir -p "/usr/share/gnome-shell/search-providers"
|
|
25
|
+
cp "$DATA_DIR/$SEARCH_PROVIDER_FILE" "/usr/share/gnome-shell/search-providers/"
|
|
26
|
+
echo "Installed: /usr/share/gnome-shell/search-providers/$SEARCH_PROVIDER_FILE"
|
|
27
|
+
|
|
28
|
+
# Install DBus service system-wide
|
|
29
|
+
mkdir -p "/usr/share/dbus-1/services"
|
|
30
|
+
cp "$DATA_DIR/$SERVICE_FILE" "/usr/share/dbus-1/services/"
|
|
31
|
+
echo "Installed: /usr/share/dbus-1/services/$SERVICE_FILE"
|
|
32
|
+
|
|
33
|
+
echo ""
|
|
34
|
+
echo "GNOME Shell Search Provider installed successfully!"
|
|
35
|
+
echo "You may need to restart GNOME Shell (Alt+F2, r) for changes to take effect."
|
|
36
|
+
echo ""
|
|
37
|
+
echo "Make sure RoonPipe daemon is running for search to work."
|