Skip to content

Commit 1b12300

Browse files
committed
beets: add simple http server to execute beets commands remotelly
1 parent 6360fcf commit 1b12300

File tree

30 files changed

+477
-76
lines changed

30 files changed

+477
-76
lines changed

.devcontainer/devcontainer.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "beets-httpshell",
3+
"image": "mcr.microsoft.com/devcontainers/python:3",
4+
"customizations": {
5+
"vscode": {
6+
"extensions": [
7+
"ms-python.python",
8+
"ms-python.vscode-pylance"
9+
],
10+
"settings": {
11+
"python.defaultInterpreterPath": "/usr/local/bin/python",
12+
"python.analysis.typeCheckingMode": "basic"
13+
}
14+
}
15+
},
16+
"forwardPorts": [5555]
17+
}

.dockerignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
.github
44
.gitattributes
55
READMETEMPLATE.md
6-
README.md
6+
README.md

.gitattributes

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414
*.pdf diff=astextplain
1515
*.PDF diff=astextplain
1616
*.rtf diff=astextplain
17-
*.RTF diff=astextplain
17+
*.RTF diff=astextplain

.github/workflows/BuildImage.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ on:
1212
env:
1313
GITHUB_REPO: "linuxserver/docker-mods" #don't modify
1414
ENDPOINT: "linuxserver/mods" #don't modify
15-
BASEIMAGE: "replace_baseimage" #replace
16-
MODNAME: "replace_modname" #replace
15+
BASEIMAGE: "beets" #replace
16+
MODNAME: "httpshell" #replace
1717
MOD_VERSION: ${{ inputs.mod_version }} #don't modify
1818
MULTI_ARCH: "true" #set to false if not needed
1919

@@ -61,4 +61,4 @@ jobs:
6161
MODNAME: ${{ needs.set-vars.outputs.MODNAME }}
6262
MULTI_ARCH: ${{ needs.set-vars.outputs.MULTI_ARCH }}
6363
MOD_VERSION: ${{ needs.set-vars.outputs.MOD_VERSION }}
64-
MOD_VERSION_OVERRIDE: ${{ needs.set-vars.outputs.MOD_VERSION_OVERRIDE }}
64+
MOD_VERSION_OVERRIDE: ${{ needs.set-vars.outputs.MOD_VERSION_OVERRIDE }}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ on:
77
- '**/check'
88
jobs:
99
permission_check:
10-
uses: linuxserver/github-workflows/.github/workflows/init-svc-executable-permissions.yml@v1
10+
uses: linuxserver/github-workflows/.github/workflows/init-svc-executable-permissions.yml@v1

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ $RECYCLE.BIN/
4040
.AppleDesktop
4141
Network Trash Folder
4242
Temporary Items
43-
.apdisk
43+
.apdisk

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
FROM scratch
44

5-
LABEL maintainer="username"
5+
LABEL maintainer="dyptan-io"
66

77
# copy local files
8-
COPY root/ /
8+
COPY root/ /

README.md

Lines changed: 223 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,231 @@
1-
# Rsync - Docker mod for openssh-server
1+
# beets-httpshell
22

3-
This mod adds rsync to openssh-server, to be installed/updated during container start.
3+
A [LinuxServer.io Docker Mod](https://github.com/linuxserver/docker-mods) for the [beets](https://github.com/linuxserver/docker-beets) container that adds a lightweight HTTP API to execute `beet` CLI commands remotely.
44

5-
In openssh-server docker arguments, set an environment variable `DOCKER_MODS=linuxserver/mods:openssh-server-rsync`
5+
The mod runs a Python 3 HTTP server (no extra dependencies) that maps URL paths to beet subcommands. Any beet command can be invoked — there is no hardcoded command list.
66

7-
If adding multiple mods, enter them in an array separated by `|`, such as `DOCKER_MODS=linuxserver/mods:openssh-server-rsync|linuxserver/mods:openssh-server-mod2`
7+
> **⚠️ Security Warning:** The HTTP API has no authentication or authorization. Any client that can reach the server can execute arbitrary beet commands. It is your responsibility to ensure the API is not exposed to untrusted networks — use firewall rules, Docker network isolation, or a reverse proxy with authentication to restrict access.
88
9-
# Mod creation instructions
9+
## Installation
1010

11-
* Fork the repo, create a new branch based on the branch `template`.
12-
* Edit the `Dockerfile` for the mod. `Dockerfile.complex` is only an example and included for reference; it should be deleted when done.
13-
* Inspect the `root` folder contents. Edit, add and remove as necessary.
14-
* After all init scripts and services are created, run `find ./ -path "./.git" -prune -o \( -name "run" -o -name "finish" -o -name "check" \) -not -perm -u=x,g=x,o=x -print -exec chmod +x {} +` to fix permissions.
15-
* Edit this readme with pertinent info, delete these instructions.
16-
* Finally edit the `.github/workflows/BuildImage.yml`. Customize the vars for `BASEIMAGE` and `MODNAME`. Set the versioning logic and `MULTI_ARCH` if needed.
17-
* Ask the team to create a new branch named `<baseimagename>-<modname>`. Baseimage should be the name of the image the mod will be applied to. The new branch will be based on the `template` branch.
18-
* Submit PR against the branch created by the team.
11+
Add the mod to your beets container using the `DOCKER_MODS` environment variable.
1912

13+
### docker run
2014

21-
## Tips and tricks
15+
```bash
16+
docker run \
17+
--name=beets \
18+
-e DOCKER_MODS=ghcr.io/linuxserver/mods:beets-httpshell \
19+
-e PUID=1000 \
20+
-e PGID=1000 \
21+
-e TZ=Europe/London \
22+
-p 8337:8337 \
23+
-p 5555:5555 \
24+
-v /path/to/config:/config \
25+
-v /path/to/music:/music \
26+
-v /path/to/downloads:/downloads \
27+
--restart unless-stopped \
28+
lscr.io/linuxserver/beets:latest
29+
```
2230

23-
* Some images have helpers built in, these images are currently:
24-
* [Openvscode-server](https://github.com/linuxserver/docker-openvscode-server/pull/10/files)
25-
* [Code-server](https://github.com/linuxserver/docker-code-server/pull/95)
31+
### docker compose
32+
33+
```yaml
34+
---
35+
services:
36+
beets:
37+
image: lscr.io/linuxserver/beets:latest
38+
container_name: beets
39+
environment:
40+
DOCKER_MODS: ghcr.io/linuxserver/mods:beets-httpshell
41+
PUID: 1000
42+
PGID: 1000
43+
TZ: Europe/London
44+
HTTPSHELL_PORT: 5555
45+
volumes:
46+
- /path/to/config:/config
47+
- /path/to/music:/music
48+
- /path/to/downloads:/downloads
49+
ports:
50+
- 8337:8337
51+
- 5555:5555
52+
restart: unless-stopped
53+
```
54+
55+
## Environment Variables
56+
57+
| Variable | Default | Description |
58+
|---|---|---|
59+
| `BEET_CMD` | `/lsiopy/bin/beet` | Path to the `beet` binary |
60+
| `BEET_CONFIG` | `/config/config.yaml` | Path to the beets config file |
61+
| `HTTPSHELL_PORT` | `5555` | Port the HTTP server listens on |
62+
| `HTTPSHELL_BLOCKING_TIMEOUT` | `30` | Seconds to wait for the lock in `block` mode before the job is queued |
63+
64+
## API Usage
65+
66+
### Execute a command
67+
68+
```
69+
POST /<command>
70+
Content-Type: application/json
71+
72+
["arg1", "arg2", ...]
73+
```
74+
75+
The URL path is the beet subcommand. The optional `?mode=` query parameter controls execution mode (`parallel`, `queue`, or `block` — defaults to `parallel`). The JSON body is an array of string arguments. An empty body or `[]` means no arguments.
76+
77+
**Response** (200 OK):
78+
79+
```json
80+
{
81+
"command": "stats",
82+
"args": [],
83+
"exit_code": 0,
84+
"stdout": "Tracks: 1234\nTotal time: 3.2 days\n...",
85+
"stderr": ""
86+
}
87+
```
88+
89+
### Health check
90+
91+
```
92+
GET /health
93+
```
94+
95+
Returns `200 OK` with server status:
96+
97+
```json
98+
{
99+
"status": "ok",
100+
"default_mode": "parallel",
101+
"queue_size": 0
102+
}
103+
```
104+
105+
### Examples
106+
107+
```bash
108+
# Get library stats (default parallel mode)
109+
curl -X POST http://localhost:5555/stats
110+
111+
# List all tracks by an artist
112+
curl -X POST http://localhost:5555/list \
113+
-H "Content-Type: application/json" \
114+
-d '["artist:Radiohead"]'
115+
116+
# Import music in parallel (returns result when done, runs in parallel with other requests)
117+
curl -X POST http://localhost:5555/import \
118+
-H "Content-Type: application/json" \
119+
-d '["/downloads/music", "--quiet", "--incremental"]'
120+
121+
# Queue an import (returns 202 immediately, runs in background)
122+
curl -X POST 'http://localhost:5555/import?mode=queue' \
123+
-H "Content-Type: application/json" \
124+
-d '["/downloads/music"]'
125+
126+
# Update the library
127+
curl -X POST http://localhost:5555/update
128+
129+
# Get beets configuration
130+
curl -X POST http://localhost:5555/config
131+
132+
# Remove tracks matching a query (force, delete files)
133+
curl -X POST http://localhost:5555/remove \
134+
-H "Content-Type: application/json" \
135+
-d '["artist:test", "-d", "-f"]'
136+
137+
# Move items to a new directory
138+
curl -X POST http://localhost:5555/move \
139+
-H "Content-Type: application/json" \
140+
-d '["artist:Radiohead", "-d", "/music/favorites"]'
141+
142+
# Health check
143+
curl http://localhost:5555/health
144+
```
145+
146+
## Execution Modes
147+
148+
The execution mode is controlled per-request via the `?mode=` query parameter. If omitted, defaults to `parallel`.
149+
150+
### `parallel` (default)
151+
152+
Each request runs its command immediately in its own thread. Multiple commands execute in parallel. The response is returned when the command finishes.
153+
154+
```
155+
Request 1 ──▶ [runs command] ──▶ 200 response
156+
Request 2 ──▶ [runs command] ──▶ 200 response (runs in parallel)
157+
```
158+
159+
### `block`
160+
161+
Each request waits for a global lock. If the lock is acquired within `HTTPSHELL_BLOCKING_TIMEOUT` seconds, the command runs and the result is returned (200). If the timeout expires, the job is queued and a 202 is returned instead. This ensures commands run one at a time.
162+
163+
```
164+
Request 1 ──▶ [acquires lock, runs command] ──▶ 200 response
165+
Request 2 ──▶ [waits for lock... acquired] ──▶ 200 response
166+
Request 3 ──▶ [waits for lock... timeout] ──▶ 202 (queued)
167+
```
168+
169+
### `queue`
170+
171+
Every request returns `202 Accepted` immediately. Commands are placed in a FIFO queue and executed one at a time by a background worker. Useful for commands that shouldn't overlap (e.g., `import`).
172+
173+
```
174+
Request 1 ──▶ 202 (queued, position 1)
175+
Request 2 ──▶ 202 (queued, position 2)
176+
[worker runs command 1, then command 2]
177+
```
178+
179+
**202 Response:**
180+
181+
```json
182+
{
183+
"status": "queued",
184+
"command": "import",
185+
"args": ["/downloads/album"],
186+
"queue_size": 1
187+
}
188+
```
189+
190+
## Lidarr Integration
191+
192+
Use beets-httpshell as a Lidarr custom script to automatically import downloads. In Lidarr, go to **Settings → Connect → +** and add a **Custom Script** with the path to the script below.
193+
194+
Create the script at a path accessible to Lidarr (e.g., `/config/scripts/beets-import.sh`):
195+
196+
```bash
197+
#!/usr/bin/env bash
198+
199+
if [ -z "$lidarr_sourcepath" ]; then
200+
echo "Error: lidarr_sourcepath environment variable not set"
201+
exit 1
202+
fi
203+
204+
curl -X POST --fail-with-body \
205+
-H "Content-Type: application/json" \
206+
-d "[\"$lidarr_sourcepath\"]" \
207+
'http://beets:5555/import?mode=block'
208+
209+
if [ $? -ne 0 ]; then
210+
echo "Import request failed"
211+
exit 1
212+
fi
213+
```
214+
215+
> **Note:** The script uses `?mode=block` so Lidarr waits for the import to complete before proceeding. Without it, the default `parallel` mode would also work but allows concurrent imports. Adjust the hostname (`beets`) and port (`5555`) to match your setup.
216+
217+
## Mod Structure
218+
219+
```text
220+
root/
221+
├── usr/local/bin/
222+
│ └── beets-httpshell.py # HTTP server script
223+
└── etc/s6-overlay/s6-rc.d/
224+
├── init-mod-beets-httpshell/ # oneshot init (startup banner, env validation)
225+
├── svc-mod-beets-httpshell/ # longrun service (HTTP server)
226+
├── init-mods-end/dependencies.d/
227+
│ └── init-mod-beets-httpshell
228+
└── user/contents.d/
229+
├── init-mod-beets-httpshell
230+
└── svc-mod-beets-httpshell
231+
```

root/etc/s6-overlay/s6-rc.d/init-mod-imagename-modname-add-package/dependencies.d/init-mods renamed to root/etc/s6-overlay/s6-rc.d/init-mod-beets-httpshell/dependencies.d/init-mods

File renamed without changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/with-contenv bash
2+
3+
echo "**** installing beets-httpshell mod ****"
4+
echo "**** httpshell port: ${HTTPSHELL_PORT:-5555} ****"
5+
echo "**** beets-httpshell mod installed ****"

0 commit comments

Comments
 (0)