roonpipe 1.0.8 → 1.0.10

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 CHANGED
@@ -11,6 +11,7 @@ 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
  - **Library Search** — Search your entire Roon library (tracks, albums, artists, playlists, and works from local and streaming services)
14
+ - **Frequency-based Re-ranking** — Frequently played items are boosted in search results, and items missing from Roon's results are injected based on your play history
14
15
  - **GNOME Search Integration** — Search and play music directly from the GNOME overview or search bar
15
16
  - **Unix Socket API** — Integrate with other applications using a simple JSON-based IPC protocol
16
17
  - **Interactive CLI** — Search and play music from your terminal with arrow key navigation and action menus
@@ -39,8 +40,8 @@ Found 50 result(s):
39
40
 
40
41
  ? Select an item to play: 💿 The Wall · Pink Floyd
41
42
  ? What do you want to do?
42
- ❯ ▶️ Play Now
43
- ⏭️ Add Next
43
+ ❯ ▶️ Play Now
44
+ ⏭️ Add Next
44
45
  📋 Queue
45
46
  📻 Start Radio
46
47
  ```
@@ -171,6 +172,7 @@ Response:
171
172
  "subtitle": "",
172
173
  "item_key": "100:0",
173
174
  "image": "/home/user/.cache/roonpipe/images/abc123.jpg",
175
+ "image_key": "abc123",
174
176
  "hint": "list",
175
177
  "sessionKey": "search_1234567890",
176
178
  "type": "artist",
@@ -187,6 +189,7 @@ Response:
187
189
  "subtitle": "The Beatles",
188
190
  "item_key": "101:0",
189
191
  "image": "/home/user/.cache/roonpipe/images/def456.jpg",
192
+ "image_key": "def456",
190
193
  "hint": "list",
191
194
  "sessionKey": "search_1234567890",
192
195
  "type": "album",
@@ -204,6 +207,7 @@ Response:
204
207
  "subtitle": "The Beatles",
205
208
  "item_key": "102:0",
206
209
  "image": "/home/user/.cache/roonpipe/images/ghi789.jpg",
210
+ "image_key": "ghi789",
207
211
  "hint": "action_list",
208
212
  "sessionKey": "search_1234567890",
209
213
  "type": "track",
@@ -213,6 +217,7 @@ Response:
213
217
  {"title": "Play Now"},
214
218
  {"title": "Add Next"},
215
219
  {"title": "Queue"},
220
+ {"title": "Play Album"},
216
221
  {"title": "Start Radio"}
217
222
  ]
218
223
  }
@@ -224,11 +229,13 @@ Response:
224
229
  - `title` — Item title
225
230
  - `subtitle` — Additional info (artist names are automatically parsed from Roon's internal format)
226
231
  - `item_key` — Item identifier (ephemeral, valid only within session context)
232
+ - `image` — Local path to cached artwork, or `null`
233
+ - `image_key` — Roon image key used for artwork caching and deduplication
227
234
  - `type` — Item type: `"artist"`, `"album"`, `"track"`, `"composer"`, `"playlist"`, or `"work"`
228
235
  - `actions` — List of available Roon actions for this item (known actions based on type)
229
236
  - `category_key` — Key to the category for navigation
230
237
  - `index` — Position within the category
231
- - `sessionKey` — Search session identifier
238
+ - `sessionKey` — Search session identifier (`"stored"` for items injected from play history)
232
239
 
233
240
  ### Play
234
241
 
@@ -252,6 +259,9 @@ Response:
252
259
  - `category_key` — Category key from search results
253
260
  - `item_index` — Index from search results
254
261
  - `action_title` — Title of the action to execute (e.g., "Play Now", "Queue", "Add Next")
262
+ - `item_title` — Item title (optional, used for frequency tracking and resolving injected items)
263
+ - `item_type` — Item type (optional, used for frequency tracking)
264
+ - `item_image_key` — Image key (optional, used for frequency tracking and resolving injected items)
255
265
 
256
266
  **How It Works:**
257
267
  The play command navigates back to the item using `category_key` and `item_index` to get fresh, valid keys, then navigates through the action hierarchy to execute the action matching `action_title`. This approach works around Roon's ephemeral browse keys.
@@ -262,6 +272,7 @@ The play command navigates back to the item using `category_key` and `item_index
262
272
  src/
263
273
  ├── index.ts # Entry point, daemon/CLI mode switching
264
274
  ├── roon.ts # Roon API connection and browsing
275
+ ├── frequency.ts # Play frequency tracking and search re-ranking
265
276
  ├── mpris.ts # MPRIS player and metadata
266
277
  ├── notification.ts # Desktop notifications
267
278
  ├── socket.ts # Unix socket server
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"startCLI",{enumerable:!0,get:function(){return u}});let e=/*#__PURE__*/o(require("node:net")),t=/*#__PURE__*/o(require("node:readline")),n=require("@inquirer/prompts");function o(e){return e&&e.__esModule?e:{default:e}}function r(t){return new Promise((n,o)=>{let r=e.default.createConnection("/tmp/roonpipe.sock",()=>{r.write(JSON.stringify(t))}),a="";r.on("data",e=>{a+=e.toString()}),r.on("end",()=>{try{let e=JSON.parse(a);e.error?o(e.error):n(e)}catch{o("Failed to parse response")}}),r.on("error",e=>{o(`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: ",n=>{e.close(),t(n)})})}async function i(){let e=await a();if(!e.trim())return[];console.log(`
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"startCLI",{enumerable:!0,get:function(){return u}});let e=/*#__PURE__*/n(require("node:net")),t=/*#__PURE__*/n(require("node:readline")),o=require("@inquirer/prompts");function n(e){return e&&e.__esModule?e:{default:e}}function r(t){return new Promise((o,n)=>{let r=e.default.createConnection("/tmp/roonpipe.sock",()=>{r.write(JSON.stringify(t))}),i="";r.on("data",e=>{i+=e.toString()}),r.on("end",()=>{try{let e=JSON.parse(i);e.error?n(e.error):o(e)}catch{n("Failed to parse response")}}),r.on("error",e=>{n(`Cannot connect to RoonPipe daemon. Is it running?
2
+ ${e.message}`)})})}async function i(){let e=t.default.createInterface({input:process.stdin,output:process.stdout});return new Promise(t=>{e.question("🔍 Search: ",o=>{e.close(),t(o)})})}async function a(){let e=await i();if(!e.trim())return[];console.log(`
3
3
  Searching for "${e}"...
4
- `);try{return(await r({command:"search",query:e})).results||[]}catch(e){return console.error("❌ Error:",e),[]}}async function l(e){let t={track:"🎵",album:"💿",artist:"🎤",playlist:"📋",work:"🎼",composer:"👤"},o=[...e.map((e,n)=>({name:`${t[e.type]||"•"} ${e.title} ${e.subtitle?`\xb7 ${e.subtitle}`:""}`,value:n})),new n.Separator,{name:"🔍 New search",value:-1},{name:"❌ Quit",value:-2}];try{let t=await (0,n.select)({message:"Select an item to play:",choices:o,pageSize:15,theme:{prefix:""}});if(-2===t)return null;if(-1===t)return{item_key:"",sessionKey:"",title:"",subtitle:"__search__",type:"track",category_key:"",index:0,actions:[]};return e[t]}catch{return null}}async function s(e){try{let t={"Play Now":"▶️",Play:"▶️",Shuffle:"🔀",Queue:"📋","Add to Queue":"📋","Add Next":"⏭️","Play From Here":"⏭️","Start Radio":"📻"},o=e.map(e=>({name:`${t[e.title]||"•"} ${e.title}`,value:e})),r=await (0,n.select)({message:"What do you want to do?",choices:[...o,new n.Separator,{name:"← Back",value:"back"}],theme:{prefix:""}});return"back"===r?null:r}catch{return null}}async function c(e,t){console.log(`
4
+ `);try{return(await r({command:"search",query:e})).results||[]}catch(e){return console.error("❌ Error:",e),[]}}async function l(e){let t={track:"🎵",album:"💿",artist:"🎤",playlist:"📋",work:"🎼",composer:"👤"},n=[...e.map((e,o)=>({name:`${t[e.type]||"•"} ${e.title} ${e.subtitle?`\xb7 ${e.subtitle}`:""}`,value:o})),new o.Separator,{name:"🔍 New search",value:-1},{name:"❌ Quit",value:-2}];try{let t=await (0,o.select)({message:"Select an item to play:",choices:n,pageSize:15,theme:{prefix:""}});if(-2===t)return null;if(-1===t)return{item_key:"",image_key:"",sessionKey:"",title:"",subtitle:"__search__",type:"track",category_key:"",index:0,actions:[]};return e[t]}catch{return null}}async function c(e){try{let t={"Play Now":"▶️","Play Album":"💿",Play:"▶️",Shuffle:"🔀",Queue:"📋","Add to Queue":"📋","Add Next":"⏭️","Play From Here":"⏭️","Start Radio":"📻","Remove from History":"🗑️"},n=e.map(e=>({name:`${t[e.title]||"•"} ${e.title}`,value:e})),r=await (0,o.select)({message:"What do you want to do?",choices:[...n,new o.Separator,{name:"← Back",value:"back"}],theme:{prefix:""}});return"back"===r?null:r}catch{return null}}async function s(e,t){console.log(`
5
5
  ${t.title}: ${e.title}${e.subtitle?` \xb7 ${e.subtitle}`:""}
6
- `);try{await r({command:"play",item_key:e.item_key,session_key:e.sessionKey,category_key:e.category_key,item_index:e.index,action_title:t.title}),console.log("✅ Success!\n")}catch(e){console.error("❌ Failed:",e)}}async function u(){for(console.log("\n🎵 RoonPipe Interactive Search"),console.log("==============================\n");;){let e=await i();if(!e.length){console.log("❌ No results found.\n");continue}console.log(`Found ${e.length} result(s):
7
- `);let t=await l(e);if(!t){console.log("\nGoodbye! 👋\n");break}if("__search__"===t.subtitle)continue;if(0===t.actions.length){console.log("No actions available for this item.\n");continue}let n=await s(t.actions);n&&await c(t,n)}}
6
+ `);try{await r({command:"play",item_key:e.item_key,session_key:e.sessionKey,category_key:e.category_key,item_index:e.index,action_title:t.title,item_title:e.title,item_type:e.type,item_image_key:e.image_key}),console.log("✅ Success!\n")}catch(e){console.error("❌ Failed:",e)}}async function u(){for(console.log("\n🎵 RoonPipe Interactive Search"),console.log("==============================\n");;){let e=await a();if(!e.length){console.log("❌ No results found.\n");continue}console.log(`Found ${e.length} result(s):
7
+ `);let t=await l(e);if(!t){console.log("\nGoodbye! 👋\n");break}if("__search__"===t.subtitle)continue;if(0===t.actions.length){console.log("No actions available for this item.\n");continue}let o=await c(t.actions);if(o){if("remove_frequency"===o.command){try{await r({command:"remove_frequency",item_title:t.title,item_type:t.type,item_image_key:t.image_key}),console.log("✅ Removed from history.\n")}catch(e){console.error("❌ Failed:",e)}continue}await s(t,o)}}}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=exports,t={get loadFrequencyData(){return f},get reRankResults(){return p},get recordPlay(){return g},get removeFrequencyEntry(){return _}};for(var r in t)Object.defineProperty(e,r,{enumerable:!0,get:Object.getOwnPropertyDescriptor(t,r).get});let i=/*#__PURE__*/s(require("node:fs")),n=/*#__PURE__*/s(require("node:os")),o=/*#__PURE__*/s(require("node:path")),a=require("./image-cache"),l=require("./roon");function s(e){return e&&e.__esModule?e:{default:e}}let c=o.default.join(n.default.homedir(),".cache","roonpipe","frequency.json"),u={version:1,items:{}},y=null;function m(e,t,r){return`${e}::${t}::${r}`}function d(){try{let e=o.default.dirname(c);i.default.existsSync(e)||i.default.mkdirSync(e,{recursive:!0}),i.default.writeFileSync(c,JSON.stringify(u,null,2))}catch(e){console.error("Failed to save frequency data:",e)}}function f(){try{if(i.default.existsSync(c)){let e=JSON.parse(i.default.readFileSync(c,"utf-8"));if(e?.version===1&&e?.items){u=e;let t=Date.now()-15552e6;for(let[e,r]of Object.entries(u.items))r.lastPlayed<t&&delete u.items[e];d()}}}catch(e){console.error("Failed to load frequency data, starting fresh:",e),u={version:1,items:{}}}}function g(e){try{let t=m(e.type,e.title,e.image_key),r=u.items[t];u.items[t]={title:e.title,subtitle:e.subtitle,type:e.type,image:e.image,image_key:e.image_key,count:(r?.count||0)+1,lastPlayed:Date.now()},y&&clearTimeout(y),y=setTimeout(d,2e3)}catch(e){console.error("Failed to record play:",e)}}function _(e,t,r){try{let i=m(e,t,r);if(i in u.items)return delete u.items[i],d(),!0;return!1}catch(e){return console.error("Failed to remove frequency entry:",e),!1}}function p(e,t){try{let r=e.toLowerCase(),i=new Set(t.map(e=>`${e.title}::${e.image_key}`)),n={title:"Remove from History",command:"remove_frequency"},o=[];for(let[,e]of Object.entries(u.items)){if(!e.title.toLowerCase().includes(r))continue;let t=`${e.title}::${e.image_key}`;if(i.has(t))continue;let s=e.image_key&&(0,a.isImageCached)(e.image_key)?(0,a.getImageCachePath)(e.image_key):e.image;o.push({title:e.title,subtitle:e.subtitle,item_key:"",image:s,image_key:e.image_key,hint:"",sessionKey:"stored",type:e.type,category_key:"",index:0,actions:[...(0,l.getKnownActions)(e.type,"action_list"),n]})}let s=new Map,c=new Set;for(let[,e]of Object.entries(u.items)){let t=`${e.title}::${e.image_key}`;s.set(t,function(e){let t=(Date.now()-e.lastPlayed)/864e5;return Math.log2(1+e.count)*.5**(t/30)}(e)),c.add(t)}for(let e of t)c.has(`${e.title}::${e.image_key}`)&&(e.actions=[...e.actions,n]);let y=[...t,...o],m=y.length;return y.sort((e,r)=>{let i=`${e.title}::${e.image_key}`,n=`${r.title}::${r.image_key}`,o=s.get(i)||0,a=s.get(n)||0,l=t.indexOf(e),c=t.indexOf(r);return(c>=0?.5*(1-c/m):0)+a-((l>=0?.5*(1-l/m):0)+o)}),y}catch(e){return console.error("Failed to re-rank results:",e),t}}
@@ -1 +1 @@
1
- "use strict";var e,t;Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"initGnomeSearchProvider",{enumerable:!0,get:function(){return g}});let r=(t=require("dbus-next"))&&t.__esModule?t:{default:t},n="com.bluemancz.RoonPipe.SearchProvider",a=new Map,i=new Map,o=null,l=null,s=null,u=(e=async(e,t)=>{let r=i.get(e);if(r)return void t(r);try{if(!l)return void t([]);let r=await l(e),n=[];for(let e of r.slice(0,5)){let t=`roon_${e.item_key}`;a.set(t,e),n.push(t)}i.set(e,n),t(n)}catch(e){console.error("Search failed:",e),t([])}},(...t)=>{o&&clearTimeout(o),o=setTimeout(()=>e(...t),300)});async function c(e){let t=e.join(" ");return t.length<4||!l?[]:new Promise(e=>{u(t,e)})}let{Interface:d}=r.default.interface;class S extends d{constructor(){super("org.gnome.Shell.SearchProvider2")}async GetInitialResultSet(e){return c(e)}async GetSubsearchResultSet(e,t){return c(t)}async GetResultMetas(e){let t=r.default.Variant,n=[];for(let r of e){let e=a.get(r);if(e){let a={id:new t("s",r),name:new t("s",e.title),description:new t("s",e.subtitle)};e.image&&(a.gicon=new t("s",e.image)),n.push(a)}}return n}async ActivateResult(e,t,r){let n=a.get(e);if(n&&s)try{if(n.actions.length>0){let e=n.actions.find(e=>"Play Now"===e.title)||n.actions.find(e=>"Shuffle"===e.title)||n.actions[0];await s(n.item_key,n.sessionKey,n.category_key,n.index,e.title),console.log(`Playing: ${n.title}`)}}catch(e){console.error("Failed to play track:",e)}}async LaunchSearch(e,t){console.log("LaunchSearch called - not implemented")}}async function g(e,t){l=e,s=t;try{let e=r.default.sessionBus();await e.requestName(n,0);let t=new S;e.export("/com/bluemancz/RoonPipe/SearchProvider",t),console.log("GNOME Search Provider initialized on",n)}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:""}}});
1
+ "use strict";var e,t;Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"initGnomeSearchProvider",{enumerable:!0,get:function(){return S}});let r=(t=require("dbus-next"))&&t.__esModule?t:{default:t},n="com.bluemancz.RoonPipe.SearchProvider",a=new Map,i=new Map,o=null,l=null,s=null,u=(e=async(e,t)=>{let r=i.get(e);if(r)return void t(r);try{if(!l)return void t([]);let r=await l(e),n=[];for(let e of r.slice(0,5)){let t=`roon_${e.item_key}`;a.set(t,e),n.push(t)}i.set(e,n),t(n)}catch(e){console.error("Search failed:",e),t([])}},(...t)=>{o&&clearTimeout(o),o=setTimeout(()=>e(...t),300)});async function c(e){let t=e.join(" ");return t.length<4||!l?[]:new Promise(e=>{u(t,e)})}let{Interface:d}=r.default.interface;class g extends d{constructor(){super("org.gnome.Shell.SearchProvider2")}async GetInitialResultSet(e){return c(e)}async GetSubsearchResultSet(e,t){return c(t)}async GetResultMetas(e){let t=r.default.Variant,n=[];for(let r of e){let e=a.get(r);if(e){let a={id:new t("s",r),name:new t("s",e.title),description:new t("s",e.subtitle)};e.image&&(a.gicon=new t("s",e.image)),n.push(a)}}return n}async ActivateResult(e,t,r){let n=a.get(e);if(n&&s)try{if(n.actions.length>0){let e=n.actions.find(e=>"Play Now"===e.title)||n.actions.find(e=>"Shuffle"===e.title)||n.actions[0];await s(n.item_key,n.sessionKey,n.category_key,n.index,e.title,n.title,n.type,n.image_key),console.log(`Playing: ${n.title}`)}}catch(e){console.error("Failed to play track:",e)}}async LaunchSearch(e,t){console.log("LaunchSearch called - not implemented")}}async function S(e,t){l=e,s=t;try{let e=r.default.sessionBus();await e.requestName(n,0);let t=new g;e.export("/com/bluemancz/RoonPipe/SearchProvider",t),console.log("GNOME Search Provider initialized on",n)}catch(e){console.error("Failed to initialize GNOME Search Provider:",e)}}g.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/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});let e=require("node:child_process"),o=require("node:fs"),r=require("node:path"),n=require("./cli"),s=require("./gnome-search-provider"),i=require("./image-cache"),t=require("./mpris"),a=require("./notification"),c=require("./roon"),l=require("./socket"),u=process.argv.includes("--install-gnome"),d=process.argv.includes("--cli");function p(){return(process.env.XDG_CURRENT_DESKTOP?.includes("GNOME")??!1)||(process.env.DESKTOP_SESSION?.includes("gnome")??!1)}if(u){let o=(0,r.join)(__dirname,"../scripts/install-gnome-search-provider.sh");process.getuid&&0!==process.getuid()?(console.log("To install GNOME Search Provider, you need to run this command as root or with sudo:"),console.log(`sudo bash "${o}"`),process.exit(1)):(console.log("Installing GNOME Search Provider..."),(0,e.execSync)(`bash "${o}"`,{stdio:"inherit"}),process.exit(0))}d?(0,n.startCLI)():(process.getuid&&0===process.getuid()&&(console.error("❌ Running as root. Please run as a regular user for the daemon."),process.exit(1)),(0,l.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,i.clearOldCache)(),p()&&((0,o.existsSync)("/usr/share/gnome-shell/search-providers/com.bluemancz.RoonPipe.SearchProvider.ini")||(console.warn("⚠️ GNOME Search Provider not installed."),console.warn(" Run 'roonpipe --install-gnome' to enable searching from GNOME overview."))),(0,t.initMpris)(()=>(0,c.getCore)()?.services.RoonApiTransport,c.getZone),(0,c.initRoon)({onCorePaired:e=>{(0,l.startSocketServer)({search:c.searchRoon,play:c.playItem}),p()&&(0,s.initGnomeSearchProvider)(c.searchRoon,c.playItem)},onCoreUnpaired:e=>{(0,t.updateMprisMetadata)(null,null)},onZoneChanged:(e,o)=>{(0,t.updateMprisMetadata)(e,o),(0,a.showTrackNotification)(e,o)},onSeekChanged:e=>{(0,t.updateMprisSeek)(e)}})}));
2
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});let e=require("node:child_process"),r=require("node:fs"),o=require("node:path"),n=require("./cli"),s=require("./frequency"),i=require("./gnome-search-provider"),t=require("./image-cache"),a=require("./mpris"),c=require("./notification"),l=require("./roon"),u=require("./socket"),d=process.argv.includes("--install-gnome"),p=process.argv.includes("--cli");function h(){return(process.env.XDG_CURRENT_DESKTOP?.includes("GNOME")??!1)||(process.env.DESKTOP_SESSION?.includes("gnome")??!1)}if(d){let r=(0,o.join)(__dirname,"../scripts/install-gnome-search-provider.sh");process.getuid&&0!==process.getuid()?(console.log("To install GNOME Search Provider, you need to run this command as root or with sudo:"),console.log(`sudo bash "${r}"`),process.exit(1)):(console.log("Installing GNOME Search Provider..."),(0,e.execSync)(`bash "${r}"`,{stdio:"inherit"}),process.exit(0))}p?(0,n.startCLI)():(process.getuid&&0===process.getuid()&&(console.error("❌ Running as root. Please run as a regular user for the daemon."),process.exit(1)),(0,u.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,t.clearOldCache)(),(0,s.loadFrequencyData)(),h()&&((0,r.existsSync)("/usr/share/gnome-shell/search-providers/com.bluemancz.RoonPipe.SearchProvider.ini")||(console.warn("⚠️ GNOME Search Provider not installed."),console.warn(" Run 'roonpipe --install-gnome' to enable searching from GNOME overview."))),(0,a.initMpris)(()=>(0,l.getCore)()?.services.RoonApiTransport,l.getZone),(0,l.initRoon)({onCorePaired:e=>{(0,u.startSocketServer)({search:l.searchRoon,play:l.playItem}),h()&&(0,i.initGnomeSearchProvider)(l.searchRoon,l.playItem)},onCoreUnpaired:e=>{(0,a.updateMprisMetadata)(null,null)},onZoneChanged:(e,r)=>{(0,a.updateMprisMetadata)(e,r),(0,c.showTrackNotification)(e,r)},onSeekChanged:e=>{(0,a.updateMprisSeek)(e)}})}));
package/dist/roon.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=exports,t={get getCore(){return y},get getZone(){return g},get initRoon(){return d},get parseNowPlaying(){return _},get playItem(){return m},get searchRoon(){return p}};for(var i in t)Object.defineProperty(e,i,{enumerable:!0,get:Object.getOwnPropertyDescriptor(t,i).get});let n=/*#__PURE__*/s(require("node-roon-api")),o=/*#__PURE__*/s(require("node-roon-api-browse")),r=/*#__PURE__*/s(require("node-roon-api-image")),a=/*#__PURE__*/s(require("node-roon-api-transport")),l=require("./image-cache");function s(e){return e&&e.__esModule?e:{default:e}}let c=null,u=null,_=e=>{let t=e.three_line?.line1||"Unknown Track";return{title:t,artists:e.three_line?.line2?[e.three_line.line2.split(" / ").map(e=>e.trim())[0]]:["Unknown Artist"],album:e.three_line?.line3||""}};function d(e){let t=new n.default({extension_id:"com.bluemancz.roonpipe",display_name:"RoonPipe",display_version:"1.0.8",publisher:"BlueManCZ",email:"your@email.com",website:"https://github.com/bluemancz/roonpipe",log_level:"none",core_paired:t=>{u=t,t.services.RoonApiTransport.subscribe_zones((i,n)=>{if("Subscribed"===i)c=n.zones.find(e=>"playing"===e.state)||n.zones[0],e.onZoneChanged(c,t);else if("Changed"===i){if(n.zones_changed){let i=n.zones_changed.find(e=>"playing"===e.state);i?c=i:c&&(c=n.zones_changed.find(e=>e.zone_id===c.zone_id)||c),e.onZoneChanged(c,t)}if(n.zones_seek_changed){let i=n.zones_seek_changed.find(e=>e.zone_id===c?.zone_id);i&&c?.now_playing&&(c.now_playing.seek_position=i.seek_position,e.onSeekChanged(1e6*i.seek_position),n.zones_changed||"playing"===c.state||(c.state="playing",e.onZoneChanged(c,t)))}}}),e.onCorePaired(t),console.log(`Core paired: ${t.display_name}`)},core_unpaired:t=>{c=null,u=null,e.onCoreUnpaired(t),console.log(`Core unpaired: ${t.display_name}`)}});t.init_services({required_services:[o.default,r.default,a.default]}),t.start_discovery()}function f(e,t){return new Promise((i,n)=>{e(t,(e,t)=>{e?n(e):i(t)})})}async function p(e){if(!u)throw Error("Roon Core not connected");if(!c)throw Error("No active zone");let t=u.services.RoonApiBrowse,i=`search_${Date.now()}`,n=(e={})=>({hierarchy:"search",multi_session_key:i,zone_or_output_id:c.zone_id,...e}),o=await f(t.browse.bind(t),n({input:e})),r=await f(t.load.bind(t),n({input:e,offset:0,count:o.list.count})),a=[];for(let e of r.items){if(!e.title)continue;let i=n({item_key:e.item_key}),o=await f(t.browse.bind(t),i),r=await f(t.load.bind(t),{...i,offset:0,count:Math.min(o.list.count,5)}),s=r.items?.map(e=>e.image_key).filter(Boolean)||[],c=await (0,l.cacheImages)(u.services.RoonApiImage,s),_=e.title.toLowerCase(),d=_.includes("composer")||_.includes("artist");a.push({category:e,items:r.items||[],cachedImages:c,isArtistCategory:d})}let s=new Map;for(let{items:e,cachedImages:t,isArtistCategory:i}of a)if(i)for(let i of e){let e=t.get(i.image_key);i.title&&e&&s.set(i.title,e)}let _=[];for(let{category:e,items:t,cachedImages:n,isArtistCategory:o}of a){if(o)continue;let r=function(e){let t=e.toLowerCase();return["artist","album","composer","playlist","track","work"].find(e=>t.includes(e))||"track"}(e.title);for(let o=0;o<t.length;o++){var d;let a=t[o],l="action_list"===a.hint&&"Play Artist"===a.title,c=l?"artist":r,u=function(e,t){let i=[{title:"Play Now"},{title:"Add Next"},{title:"Queue"},{title:"Start Radio"}],n=[{title:"Shuffle"},{title:"Start Radio"}];if("action_list"===t)return"track"===e?i:n;switch(e){case"album":case"track":return i;case"artist":case"composer":return n;case"playlist":return[{title:"Play Now"},{title:"Shuffle"},{title:"Add Next"},{title:"Queue"},{title:"Start Radio"}];default:return[]}}(c,a.hint),f=l?e.title:null,p=n.get(a.image_key)||f&&s.get(f)||null;if(_.push({title:l?e.title:a.title||`Unknown ${c.charAt(0).toUpperCase()+c.slice(1)}`,subtitle:(d=a.subtitle)?d.replace(/\[\[(\d+)\|([^\]]+)]]/g,"$2").split(", ")[0].trim():"",item_key:a.item_key,image:p,hint:a.hint,sessionKey:i,type:c,category_key:e.item_key,index:o,actions:u}),l)break}}return _}async function m(e,t,i,n,o){if(!u)throw Error("Roon Core not connected");if(!c)throw Error("No active zone");let r=u.services.RoonApiBrowse,a=(e,t={})=>({hierarchy:"search",multi_session_key:e,zone_or_output_id:c.zone_id,...t});console.log(`[DEBUG] playItem: itemKey=${e}, categoryKey=${i}, itemIndex=${n}, actionTitle=${o}`),await f(r.browse.bind(r),a(t,{item_key:i}));let l=await f(r.load.bind(r),a(t,{item_key:i,offset:n,count:1}));if(!l.items?.[0])throw Error("Item not found at index");let s=l.items[0].item_key;async function _(e,t,i=0){if(i>5)return!1;let n=await f(r.browse.bind(r),a(t,{item_key:e})),l=n.list?.multi_session_key||t,s=await f(r.load.bind(r),a(l,{item_key:e,offset:0,count:n.list?.count||50}));if(!s.items?.length)return!1;for(let e of s.items){if(console.log(`[DEBUG] Navigating: title=${e.title}, hint=${e.hint}`),"action"===e.hint&&e.title===o)return console.log(`[DEBUG] Found action! Executing: ${e.title} (${e.item_key})`),await f(r.browse.bind(r),a(l,{item_key:e.item_key})),console.log("[DEBUG] Successfully executed action"),!0;if("action_list"===e.hint||"list"===e.hint&&1===s.items.length){if(await _(e.item_key,l,i+1))return!0;if(1===i&&"action_list"===e.hint)break}}return!1}if(console.log(`[DEBUG] Got fresh item_key: ${s}`),!await _(s,t))throw Error(`Could not find action "${o}" to execute`)}function g(){return c}function y(){return u}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=exports,t={get getCore(){return w},get getKnownActions(){return g},get getZone(){return p},get initRoon(){return m},get parseNowPlaying(){return d},get playItem(){return h},get searchRoon(){return k}};for(var i in t)Object.defineProperty(e,i,{enumerable:!0,get:Object.getOwnPropertyDescriptor(t,i).get});let n=/*#__PURE__*/u(require("node-roon-api")),o=/*#__PURE__*/u(require("node-roon-api-browse")),a=/*#__PURE__*/u(require("node-roon-api-image")),r=/*#__PURE__*/u(require("node-roon-api-transport")),l=require("./frequency"),s=require("./image-cache");function u(e){return e&&e.__esModule?e:{default:e}}let c=null,_=null,d=e=>{let t=e.three_line?.line1||"Unknown Track";return{title:t,artists:e.three_line?.line2?[e.three_line.line2.split(" / ").map(e=>e.trim())[0]]:["Unknown Artist"],album:e.three_line?.line3||""}};function m(e){let t=new n.default({extension_id:"com.bluemancz.roonpipe",display_name:"RoonPipe",display_version:"1.0.10",publisher:"BlueManCZ",email:"your@email.com",website:"https://github.com/bluemancz/roonpipe",log_level:"none",core_paired:t=>{_=t,t.services.RoonApiTransport.subscribe_zones((i,n)=>{if("Subscribed"===i)c=n.zones.find(e=>"playing"===e.state)||n.zones[0],e.onZoneChanged(c,t);else if("Changed"===i){if(n.zones_changed){let i=n.zones_changed.find(e=>"playing"===e.state);i?c=i:c&&(c=n.zones_changed.find(e=>e.zone_id===c.zone_id)||c),e.onZoneChanged(c,t)}if(n.zones_seek_changed){let i=n.zones_seek_changed.find(e=>e.zone_id===c?.zone_id);i&&c?.now_playing&&(c.now_playing.seek_position=i.seek_position,e.onSeekChanged(1e6*i.seek_position),n.zones_changed||"playing"===c.state||(c.state="playing",e.onZoneChanged(c,t)))}}}),e.onCorePaired(t),console.log(`Core paired: ${t.display_name}`)},core_unpaired:t=>{c=null,_=null,e.onCoreUnpaired(t),console.log(`Core unpaired: ${t.display_name}`)}});t.init_services({required_services:[o.default,a.default,r.default]}),t.start_discovery()}function y(e,t){return new Promise((i,n)=>{e(t,(e,t)=>{e?n(e):i(t)})})}function f(e){return e?e.replace(/\[\[(\d+)\|([^\]]+)]]/g,"$2").trim():""}function g(e,t){let i=[{title:"Play Now"},{title:"Add Next"},{title:"Queue"},{title:"Play Album"},{title:"Start Radio"}],n=[{title:"Shuffle"},{title:"Start Radio"}];if("action_list"===t)return"track"===e?i:n;switch(e){case"album":return[{title:"Play Now"},{title:"Add Next"},{title:"Queue"},{title:"Start Radio"}];case"track":return i;case"artist":case"composer":return n;case"playlist":return[{title:"Play Now"},{title:"Shuffle"},{title:"Add Next"},{title:"Queue"},{title:"Start Radio"}];default:return[]}}async function k(e){if(!_)throw Error("Roon Core not connected");if(!c)throw Error("No active zone");let t=_.services.RoonApiBrowse,i=`search_${Date.now()}`,n=(e={})=>({hierarchy:"search",multi_session_key:i,zone_or_output_id:c.zone_id,...e}),o=await y(t.browse.bind(t),n({input:e})),a=await y(t.load.bind(t),n({input:e,offset:0,count:o.list.count})),r=[];for(let e of a.items){if(!e.title)continue;let i=n({item_key:e.item_key}),o=await y(t.browse.bind(t),i),a=await y(t.load.bind(t),{...i,offset:0,count:Math.min(o.list.count,5)}),l=a.items?.map(e=>e.image_key).filter(Boolean)||[],u=await (0,s.cacheImages)(_.services.RoonApiImage,l),c=e.title.toLowerCase(),d=c.includes("composer")||c.includes("artist");r.push({category:e,items:a.items||[],cachedImages:u,isArtistCategory:d})}let u=new Map;for(let{items:e,cachedImages:t,isArtistCategory:i}of r)if(i)for(let i of e){let e=t.get(i.image_key);i.title&&e&&u.set(i.title,e)}let d=[],m=new Set;for(let{category:e,items:t,cachedImages:n,isArtistCategory:o}of r){if(o)continue;let a=function(e){let t=e.toLowerCase();return["artist","album","composer","playlist","track","work"].find(e=>t.includes(e))||"track"}(e.title);for(let o=0;o<t.length;o++){let r,l=t[o],s="action_list"===l.hint&&"Play Artist"===l.title,c=g(r=s?"artist":"track"===a&&"list"===l.hint?"album":a,l.hint),_=`${l.title}::${l.image_key}`;if(m.has(_))continue;m.add(_);let y=s?e.title:null,k=n.get(l.image_key)||y&&u.get(y)||null;if(d.push({title:s?e.title:l.title||`Unknown ${r.charAt(0).toUpperCase()+r.slice(1)}`,subtitle:f(l.subtitle),item_key:l.item_key,image:k,image_key:l.image_key||"",hint:l.hint,sessionKey:i,type:r,category_key:e.item_key,index:o,actions:c}),s)break}}let k=(0,l.reRankResults)(e,d),h=k.filter(e=>"stored"===e.sessionKey&&!e.image&&e.image_key).map(e=>e.image_key);if(h.length>0){let e=await (0,s.cacheImages)(_.services.RoonApiImage,h);for(let t of k)"stored"===t.sessionKey&&!t.image&&t.image_key&&(t.image=e.get(t.image_key)||null)}return k}async function h(e,t,i,n,o,a,r,s){if(!_)throw Error("Roon Core not connected");if(!c)throw Error("No active zone");let u=_.services.RoonApiBrowse,d=(e,t={})=>({hierarchy:"search",multi_session_key:e,zone_or_output_id:c.zone_id,...t});if("stored"===t&&a){console.log(`[DEBUG] Resolving stored item: "${a}" (image_key: ${s})`);let e=(await k(a)).find(e=>e.image_key===s&&"stored"!==e.sessionKey);if(!e)throw Error(`Could not find "${a}" in fresh search results`);return h(e.item_key,e.sessionKey,e.category_key,e.index,o,e.title,e.type,e.image_key)}console.log(`[DEBUG] playItem: itemKey=${e}, categoryKey=${i}, itemIndex=${n}, actionTitle=${o}`),await y(u.browse.bind(u),d(t,{item_key:i}));let m=await y(u.load.bind(u),d(t,{item_key:i,offset:n,count:1}));if(!m.items?.[0])throw Error("Item not found at index");let g=m.items[0],p=g.item_key;if(console.log(`[DEBUG] Got fresh item_key: ${p}`),"Play Album"===o){let e=g.image_key;if(!e)throw Error("Track has no image_key — cannot identify album");let t=g.subtitle||"",i=[...t.split(",").map(e=>e.trim()).filter(Boolean)].sort((e,t)=>e.split(/\s+/).length-t.split(/\s+/).length),n=`album_${Date.now()}`,o=(e={})=>({hierarchy:"search",multi_session_key:n,zone_or_output_id:c.zone_id,...e});for(let n of i){console.log(`[DEBUG] Trying album search with artist: "${n}" (track image_key: ${e})`);try{let i=await y(u.browse.bind(u),o({input:n})),c=await y(u.load.bind(u),o({offset:0,count:i.list.count})),_=c.items?.find(e=>e.title?.toLowerCase()==="albums");if(!_)continue;let d=await y(u.browse.bind(u),o({item_key:_.item_key})),m=await y(u.load.bind(u),o({offset:0,count:Math.min(d.list.count,50)})),k=m.items?.find(t=>t.image_key===e);if(!k)continue;console.log(`[DEBUG] Found matching album: "${k.title}" (via "${n}")`),await y(u.browse.bind(u),o({item_key:k.item_key}));let h=await y(u.load.bind(u),o({offset:0,count:5})),p=h.items?.find(e=>"list"===e.hint);if(!p)continue;await y(u.browse.bind(u),o({item_key:p.item_key}));let w=await y(u.load.bind(u),o({offset:0,count:30})),b=w.items?.find(e=>"action_list"===e.hint&&"Play Album"===e.title);if(!b)continue;await y(u.browse.bind(u),o({item_key:b.item_key}));let $=await y(u.load.bind(u),o({offset:0,count:10})),E=$.items?.find(e=>"action"===e.hint&&"Play Now"===e.title);if(!E)continue;console.log(`[DEBUG] Playing album: "${k.title}"`),await y(u.browse.bind(u),o({item_key:E.item_key})),console.log("[DEBUG] Successfully started album playback"),(0,l.recordPlay)({title:g.title||a||"",subtitle:f(t),item_key:"",image:null,image_key:g.image_key||s||"",hint:"",sessionKey:"",type:r||"track",category_key:"",index:0,actions:[]});return}catch{}}throw Error("Could not find album for this track")}async function w(e,t,i=0){if(i>5)return!1;let n=await y(u.browse.bind(u),d(t,{item_key:e})),a=n.list?.multi_session_key||t,r=await y(u.load.bind(u),d(a,{item_key:e,offset:0,count:n.list?.count||50}));if(!r.items?.length)return!1;for(let e of r.items){if(console.log(`[DEBUG] Navigating: title=${e.title}, hint=${e.hint}`),"action"===e.hint&&e.title===o)return console.log(`[DEBUG] Found action! Executing: ${e.title} (${e.item_key})`),await y(u.browse.bind(u),d(a,{item_key:e.item_key})),console.log("[DEBUG] Successfully executed action"),!0;if("action_list"===e.hint||"list"===e.hint&&1===r.items.length){if(await w(e.item_key,a,i+1))return!0;if(1===i&&"action_list"===e.hint)break}}return!1}if(!await w(p,t))throw Error(`Could not find action "${o}" to execute`);(0,l.recordPlay)({title:g.title||a||"",subtitle:f(g.subtitle||""),item_key:"",image:null,image_key:g.image_key||s||"",hint:"",sessionKey:"",type:r||"track",category_key:"",index:0,actions:[]})}function p(){return c}function w(){return _}
package/dist/socket.js CHANGED
@@ -1,7 +1,8 @@
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})}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=exports,r={get isInstanceRunning(){return l},get startSocketServer(){return a}};for(var t in r)Object.defineProperty(e,t,{enumerable:!0,get:Object.getOwnPropertyDescriptor(r,t).get});let n=/*#__PURE__*/s(require("node:fs")),i=/*#__PURE__*/s(require("node:net")),o=require("./frequency");function s(e){return e&&e.__esModule?e:{default:e}}let c="/tmp/roonpipe.sock";function l(){return new Promise(e=>{if(!n.default.existsSync(c))return void e(!1);let r=i.default.createConnection({path:c},()=>{r.end(),e(!0)});r.on("error",()=>{e(!1)}),r.setTimeout(1e3,()=>{r.destroy(),e(!1)})})}function a(e){n.default.existsSync(c)&&n.default.unlinkSync(c),i.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
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.category_key,n.item_index,n.action_title),r.write(`${JSON.stringify({error:null,success:!0})}
3
+ `)}r.end()}else if("play"===n.command){try{await e.play(n.item_key,n.session_key,n.category_key,n.item_index,n.action_title,n.item_title,n.item_type,n.item_image_key),r.write(`${JSON.stringify({error:null,success:!0})}
4
4
  `)}catch(e){r.write(`${JSON.stringify({error:String(e),success:!1})}
5
- `)}r.end()}else r.write(`${JSON.stringify({error:"Unknown command"})}
5
+ `)}r.end()}else if("remove_frequency"===n.command){let e=(0,o.removeFrequencyEntry)(n.item_type,n.item_title,n.item_image_key);r.write(`${JSON.stringify({error:null,success:e})}
6
+ `),r.end()}else r.write(`${JSON.stringify({error:"Unknown command"})}
6
7
  `),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)})}
8
+ `),r.end()}}),r.on("error",e=>{console.error("Client error:",e)})}).listen(c,()=>{console.log("Unix socket server listening on",c),n.default.chmodSync(c,438)})}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roonpipe",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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": {