Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/xunia-soundcloudopen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: XUNIA SoundCloudOpen Bridge

on:
push:
branches:
- master
- feature/xunia-soundcloudopen-desktop-bridge
pull_request:
branches:
- master

jobs:
verify:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Verify XUNIA + SoundCloudOpen bridge
run: npm run check:xunia
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
# SOUNDNODE APP — XUNIA DESKTOP

## XUNIA SOUNDS + SoundCloudOpen

This fork now includes a focused desktop bridge to [SoundCloudOpen](https://github.com/sonoxo/soundcloudopen).

**Soundnode plays/browses → XUNIA SOUNDS builds the creator mission → 3LM CLAUDE can use BeatStars discovery → SoundCloudOpen handles authorized SoundCloud saving.**

Open it from the desktop menu:

**XUNIA SOUNDS → Open XUNIA SOUNDS + SoundCloudOpen**

Shortcut: `Cmd/Ctrl + Shift + X`

The new panel supports:

- SoundCloudOpen/XUNIA CLI readiness checks;
- VIRGINIA + BeatStars mission creation;
- BPM, genre, mood, use-case, candidate-count, and optional SoundCloud source controls;
- Claude prompt-only output;
- authorized SoundCloud track/playlist saving through the existing SoundCloudOpen CLI; and
- shell-safe process execution with argument arrays instead of command-string interpolation.

Install the companion CLI on the same computer:

```bash
python3 -m pip install git+https://github.com/sonoxo/soundcloudopen.git
soundcloudopen --version
xunia-sounds --version
```

Focused verification:

```bash
npm run check:xunia
```

Full integration guide: [doc/XUNIA_SOUNDCLOUDOPEN.md](doc/XUNIA_SOUNDCLOUDOPEN.md)

---

[![Join the chat at https://gitter.im/Soundnode/soundnode-app](https://badges.gitter.im/Soundnode/soundnode-app.svg)](https://gitter.im/Soundnode/soundnode-app?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

Soundnode App
Expand Down
97 changes: 97 additions & 0 deletions app/public/js/xuniaSoundCloudOpen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use strict';

const { ipcRenderer } = require('electron');

function byId(id) {
return document.getElementById(id);
}

function value(id) {
return byId(id).value.trim();
}

function missionPayload(promptOnly) {
return {
prompt: value('prompt'),
genre: value('genre'),
mood: value('mood'),
bpm: value('bpm'),
count: value('count'),
useCase: value('useCase'),
format: value('format'),
soundcloudUrl: value('soundcloudUrl'),
promptOnly: !!promptOnly
};
}

function downloadPayload() {
return {
url: value('soundcloudUrl'),
format: value('format'),
browser: value('browser'),
output: value('output')
};
}

function showOutput(payload) {
const node = byId('outputView');
node.textContent = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
}

function setStatus(message, good) {
const status = byId('status');
status.textContent = message;
status.className = `status ${good === true ? 'good' : good === false ? 'bad' : ''}`;
}

async function check() {
setStatus('Checking SoundCloudOpen…');
try {
const result = await ipcRenderer.invoke('xunia:check');
if (result.ready) {
setStatus(`READY — ${result.xunia.version} / ${result.downloader.version}`, true);
} else {
setStatus('NOT READY — install SoundCloudOpen 1.2+ on this computer.', false);
}
showOutput(result);
} catch (error) {
setStatus(error.message, false);
showOutput({ error: error.message });
}
}

async function buildMission(promptOnly) {
showOutput(promptOnly ? 'Building Claude discovery prompt…' : 'Building XUNIA mission…');
try {
const result = await ipcRenderer.invoke('xunia:mission', missionPayload(promptOnly));
showOutput(result);
} catch (error) {
showOutput({ error: error.message });
}
}

async function saveAuthorizedMedia() {
const url = value('soundcloudUrl');
if (!url) {
showOutput({ error: 'Add an authorized SoundCloud track or playlist URL first.' });
return;
}

const confirmed = window.confirm('Save this SoundCloud media only if you own it, SoundCloud permits the download, or you otherwise have permission. Continue?');
if (!confirmed) return;

showOutput('Starting SoundCloudOpen…');
try {
const result = await ipcRenderer.invoke('xunia:download', downloadPayload());
showOutput(result);
} catch (error) {
showOutput({ error: error.message });
}
}

byId('check').addEventListener('click', check);
byId('mission').addEventListener('click', () => buildMission(false));
byId('promptOnly').addEventListener('click', () => buildMission(true));
byId('save').addEventListener('click', saveAuthorizedMedia);

check();
104 changes: 104 additions & 0 deletions app/xunia.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XUNIA SOUNDS // SoundCloudOpen</title>
<style>
:root { color-scheme: dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
body { margin: 0; background: #090b13; color: #f4f7ff; }
.shell { max-width: 920px; margin: 0 auto; padding: 28px; }
h1 { margin: 0 0 8px; font-size: 28px; }
.sub { color: #a9b5d1; margin-bottom: 24px; line-height: 1.5; }
.flow { background: #111629; border: 1px solid #2a3354; border-radius: 14px; padding: 14px 16px; margin-bottom: 20px; }
.flow strong { display: block; margin-bottom: 6px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.wide { grid-column: 1 / -1; }
label { display: block; font-size: 12px; font-weight: 700; color: #b9c6e8; margin-bottom: 6px; }
input, select, textarea { width: 100%; box-sizing: border-box; border-radius: 10px; border: 1px solid #303b60; background: #0f1423; color: white; padding: 10px 12px; }
textarea { min-height: 88px; resize: vertical; }
button { border: 0; border-radius: 10px; padding: 11px 14px; font-weight: 800; cursor: pointer; }
.primary { background: #8b5cf6; color: white; }
.secondary { background: #18213b; color: #dce7ff; border: 1px solid #33416d; }
.danger { background: #2a1720; color: #ffdbe6; border: 1px solid #633044; }
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 18px 0; }
.status { padding: 12px 14px; border-radius: 10px; background: #10182a; border: 1px solid #293657; margin-bottom: 14px; }
.status.good { border-color: #1d704f; }
.status.bad { border-color: #7a3345; }
pre { white-space: pre-wrap; word-break: break-word; background: #070a11; border: 1px solid #222b45; border-radius: 12px; padding: 14px; min-height: 120px; }
.note { color: #9da9c8; font-size: 12px; line-height: 1.5; }
@media (max-width: 700px) { .grid { grid-template-columns: 1fr; } .wide { grid-column: auto; } }
</style>
</head>
<body>
<main class="shell">
<h1>XUNIA SOUNDS + SoundCloudOpen</h1>
<div class="sub">Desktop bridge for Soundnode. Describe a sound, build a VIRGINIA/BeatStars mission with XUNIA SOUNDS, or save SoundCloud media you own or are allowed to save.</div>

<div class="flow">
<strong>BEGINNER FLOW</strong>
YOU DESCRIBE → XUNIA SOUNDS BUILDS THE PLAN → CLAUDE/BEATSTARS DISCOVERY → REVIEW LICENSE → OPTIONAL SOUNDCLOUDOPEN SAVE
</div>

<div id="status" class="status">Checking SoundCloudOpen…</div>

<section class="grid">
<div class="wide">
<label for="prompt">Describe the sound you want</label>
<textarea id="prompt" placeholder="dark melodic trap with cold synths and space for vocals"></textarea>
</div>
<div>
<label for="genre">Genre</label>
<input id="genre" placeholder="trap">
</div>
<div>
<label for="mood">Mood</label>
<input id="mood" placeholder="cold, nocturnal, cinematic">
</div>
<div>
<label for="bpm">BPM</label>
<input id="bpm" type="number" min="1" max="400" placeholder="90">
</div>
<div>
<label for="count">Beat candidates</label>
<input id="count" type="number" min="1" max="10" value="5">
</div>
<div>
<label for="useCase">Use case</label>
<input id="useCase" placeholder="album song">
</div>
<div>
<label for="format">Audio format</label>
<select id="format">
<option>mp3</option><option>m4a</option><option>opus</option><option>flac</option><option>wav</option><option>aac</option>
</select>
</div>
<div class="wide">
<label for="soundcloudUrl">Optional authorized SoundCloud track / playlist</label>
<input id="soundcloudUrl" placeholder="https://soundcloud.com/your-account/your-track">
</div>
<div>
<label for="browser">Browser cookies for authorized/private media</label>
<select id="browser">
<option>auto</option><option>chrome</option><option>chromium</option><option>edge</option><option>firefox</option><option>brave</option><option>opera</option><option>safari</option><option>none</option>
</select>
</div>
<div>
<label for="output">Optional output folder</label>
<input id="output" placeholder="~/Music/SoundCloudOpen">
</div>
</section>

<div class="actions">
<button id="check" class="secondary">CHECK INTEGRATION</button>
<button id="mission" class="primary">BUILD XUNIA MISSION</button>
<button id="promptOnly" class="secondary">BUILD CLAUDE PROMPT</button>
<button id="save" class="danger">SAVE AUTHORIZED SOUNDCLOUD MEDIA</button>
</div>

<div class="note">The save button does not bypass SoundCloud permissions. Use it only for audio you own, downloads SoundCloud permits, or material you otherwise have permission to save.</div>
<pre id="outputView">Ready.</pre>
</main>
<script src="./public/js/xuniaSoundCloudOpen.js"></script>
</body>
</html>
79 changes: 79 additions & 0 deletions doc/XUNIA_SOUNDCLOUDOPEN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# XUNIA SOUNDS + SoundCloudOpen Desktop Bridge

## Beginner version

This fork turns Soundnode into a desktop front end for the creator-owned SoundCloudOpen workflow.

**PLAY / BROWSE IN SOUNDNODE → OPEN XUNIA SOUNDS → DESCRIBE A SOUND → BUILD A VIRGINIA + BEATSTARS DISCOVERY MISSION → REVIEW THE BEAT/LiCENSE → OPTIONALLY SAVE SOUNDCLOUD MEDIA YOU OWN OR ARE ALLOWED TO SAVE**

The original Soundnode player remains intact. The new integration lives beside it.

## What each part does

| Part | Job |
|---|---|
| Soundnode | Desktop SoundCloud listening/browsing interface |
| XUNIA SOUNDS panel | Beginner desktop control surface |
| `integrations/soundcloudopenBridge.js` | Shell-safe bridge from Electron to the SoundCloudOpen CLI |
| `xunia-sounds` | Builds the VIRGINIA / 3LM CLAUDE / BeatStars discovery mission |
| `soundcloudopen` | Saves authorized SoundCloud tracks/playlists with the existing SoundCloudOpen rules |
| BeatStars MCP | Discovery tool provider used through Claude |

## Install the companion CLI

Soundnode does not embed Python. Install SoundCloudOpen 1.2+ on the same computer:

```bash
python3 -m pip install git+https://github.com/sonoxo/soundcloudopen.git
```

Then verify:

```bash
soundcloudopen --version
xunia-sounds --version
```

## Open the desktop panel

Start Soundnode, then use:

**XUNIA SOUNDS → Open XUNIA SOUNDS + SoundCloudOpen**

Keyboard shortcut:

```text
Cmd/Ctrl + Shift + X
```

The panel can:

1. check whether both SoundCloudOpen commands are available;
2. build a structured XUNIA SOUNDS mission;
3. build only the Claude/BeatStars natural-language prompt;
4. pass an optional authorized SoundCloud URL into the mission; and
5. run the existing SoundCloudOpen save command for authorized media.

## Security boundary

The Electron bridge never constructs a shell command string. It calls child processes with an executable plus an argument array and `shell: false`.

SoundCloudOpen remains responsible for its existing URL validation, dependency checks, browser-cookie selection, metadata handling, and yt-dlp/FFmpeg execution.

The desktop panel also validates SoundCloud hostnames before execution and asks for confirmation before starting a save.

## Verification

The integration has a dependency-free Node test lane:

```bash
npm run check:xunia
```

CI runs that verification on:

- Windows / Node 18 and 20
- macOS / Node 18 and 20
- Ubuntu / Node 18 and 20

This focused lane intentionally does not install the legacy Electron 8 / node-sass dependency tree just to verify the new bridge.
Loading