@amoshydra/davison 0.0.1
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/LICENSE +21 -0
- package/README.md +132 -0
- package/bin/davison.js +65 -0
- package/dist/assets/index-BXibJ7mX.js +165 -0
- package/dist/assets/index-DjSu-QAM.css +1 -0
- package/dist/index.html +13 -0
- package/dist-server/config.js +24 -0
- package/dist-server/index.js +88 -0
- package/dist-server/routes/api.js +259 -0
- package/dist-server/services/audio-gen.js +60 -0
- package/dist-server/services/music-discovery.js +73 -0
- package/dist-server/services/music-server.js +90 -0
- package/dist-server/services/playlist-store.js +84 -0
- package/dist-server/services/queue-manager.js +339 -0
- package/dist-server/services/sonos-controller.js +298 -0
- package/dist-server/services/webdav.js +381 -0
- package/package.json +61 -0
- package/patches/nephele.patch +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 amoshydra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# davison
|
|
2
|
+
|
|
3
|
+
A standalone music server that streams your local music library to Sonos speakers and serves it via WebDAV for tools like Music Assistant.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Sonos control** — discover Sonos speakers on your LAN, stream music directly to them
|
|
8
|
+
- **Mobile-first web UI** — browse your music library by folder, control playback, manage queues and playlists
|
|
9
|
+
- **WebDAV server** — expose your music library over WebDAV for Music Assistant, file managers, or any WebDAV client
|
|
10
|
+
- **Direct streaming** — HTTP range-request streaming with album art extraction
|
|
11
|
+
- **Zero configuration** — point it at your music folders and go
|
|
12
|
+
- **No cloud dependency** — everything runs on your local network
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Prerequisites
|
|
17
|
+
|
|
18
|
+
- Node.js 22+ (or Docker)
|
|
19
|
+
- Music files on a local or mounted drive
|
|
20
|
+
|
|
21
|
+
### Via npx
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx davison --path /path/to/music
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open http://localhost:3000 in your browser.
|
|
28
|
+
|
|
29
|
+
### Via Docker
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker run -d \
|
|
33
|
+
--name davison \
|
|
34
|
+
--network host \
|
|
35
|
+
-v /path/to/music:/music:ro \
|
|
36
|
+
ghcr.io/your-username/davison \
|
|
37
|
+
--path /music
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> `--network host` is required for Sonos UPnP discovery to work.
|
|
41
|
+
|
|
42
|
+
### Via Docker Compose
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
version: "3.8"
|
|
46
|
+
services:
|
|
47
|
+
davison:
|
|
48
|
+
image: ghcr.io/your-username/davison
|
|
49
|
+
network_mode: host
|
|
50
|
+
volumes:
|
|
51
|
+
- /path/to/music:/music:ro
|
|
52
|
+
command: ["--path", "/music"]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Command-line options
|
|
58
|
+
|
|
59
|
+
| Option | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| `-p, --path <paths...>` | One or more music directories (required) |
|
|
62
|
+
| `--port <number>` | Server port (default: 3000) |
|
|
63
|
+
| `--host <address>` | LAN address (auto-detected if omitted) |
|
|
64
|
+
|
|
65
|
+
### Web UI
|
|
66
|
+
|
|
67
|
+
| Tab | Description |
|
|
68
|
+
|-----|-------------|
|
|
69
|
+
| **Library** | Browse music by folder structure |
|
|
70
|
+
| **Queue** | View and manage the current playback queue |
|
|
71
|
+
| **Now Playing** | Full-screen player with album art, vinyl animation, swipe gestures |
|
|
72
|
+
| **Playlists** | Create, edit, and load playlists |
|
|
73
|
+
| **Settings** | Discover and select Sonos devices |
|
|
74
|
+
|
|
75
|
+
### WebDAV
|
|
76
|
+
|
|
77
|
+
The WebDAV server is available at `/webdav` (e.g., `http://192.168.0.x:3000/webdav/`). It mirrors your music folder structure. No authentication required.
|
|
78
|
+
|
|
79
|
+
To use with Music Assistant:
|
|
80
|
+
1. Add a **WebDAV** provider
|
|
81
|
+
2. URL: `http://<your-server-ip>:3000/webdav/`
|
|
82
|
+
3. Leave username/password blank
|
|
83
|
+
4. Content type: `music`
|
|
84
|
+
|
|
85
|
+
### Sonos
|
|
86
|
+
|
|
87
|
+
davison discovers Sonos speakers on your LAN automatically via SSDP multicast. Select a speaker from the **Settings** tab and start playing.
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/your-username/davison.git
|
|
93
|
+
cd davison
|
|
94
|
+
pnpm install
|
|
95
|
+
pnpm run dev -- --path /path/to/music
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The dev server hot-reloads on file changes.
|
|
99
|
+
|
|
100
|
+
## Architecture
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
┌──────────────────────┐
|
|
104
|
+
│ Web Browser │
|
|
105
|
+
│ (React + Vite) │
|
|
106
|
+
└──────────┬───────────┘
|
|
107
|
+
│ HTTP (port 3000)
|
|
108
|
+
┌──────────▼───────────┐
|
|
109
|
+
│ Express Server │
|
|
110
|
+
│ ┌─────────────────┐ │
|
|
111
|
+
│ │ REST API (/api) │ │
|
|
112
|
+
│ │ Queue Manager │ │
|
|
113
|
+
│ │ Sonos Controller│ │
|
|
114
|
+
│ │ Playlist Store │ │
|
|
115
|
+
│ ├─────────────────┤ │
|
|
116
|
+
│ │ WebDAV (/webdav)│ │
|
|
117
|
+
│ │ (Nephele VFS) │ │
|
|
118
|
+
│ └─────────────────┘ │
|
|
119
|
+
└──────────┬───────────┘
|
|
120
|
+
│
|
|
121
|
+
┌───────────────────┼───────────────────┐
|
|
122
|
+
│ │ │
|
|
123
|
+
┌──────▼──────┐ ┌──────▼──────┐ ┌───────▼──────┐
|
|
124
|
+
│ Sonos │ │ Music Files │ │ Music │
|
|
125
|
+
│ Speakers │ │ (on disk) │ │ Assistant │
|
|
126
|
+
│ (UPnP/HTTP) │ │ │ │ (via WebDAV) │
|
|
127
|
+
└─────────────┘ └─────────────┘ └──────────────┘
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
package/bin/davison.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// `npx @amoshydra/davison` entry point.
|
|
4
|
+
import { spawn, execSync } from 'node:child_process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { dirname, resolve } from 'node:path'
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const root = resolve(__dirname, '..')
|
|
11
|
+
|
|
12
|
+
// Apply the Nephele HEAD Content-Length patch if not already applied
|
|
13
|
+
const getHeadPath = resolve(root, 'node_modules', 'nephele', 'dist', 'Methods', 'GET_HEAD.js')
|
|
14
|
+
if (existsSync(getHeadPath)) {
|
|
15
|
+
const content = readFileSync(getHeadPath, 'utf8')
|
|
16
|
+
if (!content.includes("'Content-Length':")) {
|
|
17
|
+
const patchPath = resolve(root, 'patches', 'nephele.patch')
|
|
18
|
+
if (existsSync(patchPath)) {
|
|
19
|
+
try {
|
|
20
|
+
execSync(`patch -p0 < "${patchPath}"`, { cwd: resolve(root, 'node_modules', 'nephele'), stdio: 'pipe' })
|
|
21
|
+
} catch {
|
|
22
|
+
// patch wasn't applied, server still works
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Auto-build if needed (first run from git clone)
|
|
32
|
+
if (!existsSync(resolve(root, 'dist-server', 'index.js'))) {
|
|
33
|
+
const viteBin = resolve(root, 'node_modules', '.bin', 'vite')
|
|
34
|
+
const tscBin = resolve(root, 'node_modules', '.bin', 'tsc')
|
|
35
|
+
if (existsSync(viteBin) && existsSync(tscBin)) {
|
|
36
|
+
console.log('Building server and frontend...')
|
|
37
|
+
try {
|
|
38
|
+
execSync(`"${tscBin}" -p "${root}/tsconfig.server.json"`, { cwd: root, stdio: 'inherit' })
|
|
39
|
+
execSync(`"${viteBin}" build`, { cwd: root, stdio: 'inherit' })
|
|
40
|
+
} catch {
|
|
41
|
+
console.warn('Build failed — falling back to tsx source')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Try compiled server first, fall back to tsx source
|
|
47
|
+
let entryPoint = resolve(root, 'dist-server', 'index.js')
|
|
48
|
+
if (!existsSync(entryPoint)) {
|
|
49
|
+
const tsxLoader = resolve(root, 'node_modules', 'tsx', 'dist', 'loader.mjs')
|
|
50
|
+
if (existsSync(tsxLoader)) {
|
|
51
|
+
entryPoint = resolve(root, 'server', 'index.ts')
|
|
52
|
+
spawn(process.execPath, ['--import', tsxLoader, entryPoint, ...process.argv.slice(2)], {
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'production' },
|
|
55
|
+
}).on('exit', (code) => process.exit(code ?? 1))
|
|
56
|
+
} else {
|
|
57
|
+
console.error('Server not built. Run: npm run build')
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
spawn(process.execPath, [entryPoint, ...process.argv.slice(2)], {
|
|
62
|
+
stdio: 'inherit',
|
|
63
|
+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'production' },
|
|
64
|
+
}).on('exit', (code) => process.exit(code ?? 1))
|
|
65
|
+
}
|