Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 17 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
Comment thread
aptalca marked this conversation as resolved.
Outdated
"name": "beets-httpshell",
"image": "mcr.microsoft.com/devcontainers/python:3",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.analysis.typeCheckingMode": "basic"
}
}
},
"forwardPorts": [5555]
}
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
.github
.gitattributes
READMETEMPLATE.md
README.md
README.md
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.RTF diff=astextplain
6 changes: 3 additions & 3 deletions .github/workflows/BuildImage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ on:
env:
GITHUB_REPO: "linuxserver/docker-mods" #don't modify
ENDPOINT: "linuxserver/mods" #don't modify
BASEIMAGE: "replace_baseimage" #replace
MODNAME: "replace_modname" #replace
BASEIMAGE: "beets" #replace
MODNAME: "httpshell" #replace
MOD_VERSION: ${{ inputs.mod_version }} #don't modify
MULTI_ARCH: "true" #set to false if not needed
Comment thread
aptalca marked this conversation as resolved.
Outdated

Expand Down Expand Up @@ -61,4 +61,4 @@ jobs:
MODNAME: ${{ needs.set-vars.outputs.MODNAME }}
MULTI_ARCH: ${{ needs.set-vars.outputs.MULTI_ARCH }}
MOD_VERSION: ${{ needs.set-vars.outputs.MOD_VERSION }}
MOD_VERSION_OVERRIDE: ${{ needs.set-vars.outputs.MOD_VERSION_OVERRIDE }}
MOD_VERSION_OVERRIDE: ${{ needs.set-vars.outputs.MOD_VERSION_OVERRIDE }}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ on:
- '**/check'
jobs:
permission_check:
uses: linuxserver/github-workflows/.github/workflows/init-svc-executable-permissions.yml@v1
uses: linuxserver/github-workflows/.github/workflows/init-svc-executable-permissions.yml@v1
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ $RECYCLE.BIN/
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.apdisk
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

FROM scratch

LABEL maintainer="username"
LABEL maintainer="dyptan-io"

# copy local files
COPY root/ /
COPY root/ /
240 changes: 223 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,231 @@
# Rsync - Docker mod for openssh-server
# beets-httpshell

This mod adds rsync to openssh-server, to be installed/updated during container start.
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.

In openssh-server docker arguments, set an environment variable `DOCKER_MODS=linuxserver/mods:openssh-server-rsync`
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.

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`
> **⚠️ 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.

# Mod creation instructions
## Installation

* Fork the repo, create a new branch based on the branch `template`.
* Edit the `Dockerfile` for the mod. `Dockerfile.complex` is only an example and included for reference; it should be deleted when done.
* Inspect the `root` folder contents. Edit, add and remove as necessary.
* 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.
* Edit this readme with pertinent info, delete these instructions.
* Finally edit the `.github/workflows/BuildImage.yml`. Customize the vars for `BASEIMAGE` and `MODNAME`. Set the versioning logic and `MULTI_ARCH` if needed.
* 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.
* Submit PR against the branch created by the team.
Add the mod to your beets container using the `DOCKER_MODS` environment variable.
Comment thread
aptalca marked this conversation as resolved.
Outdated

### docker run

## Tips and tricks
```bash
docker run \
--name=beets \
-e DOCKER_MODS=ghcr.io/linuxserver/mods:beets-httpshell \
Comment thread
aptalca marked this conversation as resolved.
Outdated
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-p 8337:8337 \
-p 5555:5555 \
Comment thread
aptalca marked this conversation as resolved.
Outdated
-v /path/to/config:/config \
-v /path/to/music:/music \
-v /path/to/downloads:/downloads \
--restart unless-stopped \
lscr.io/linuxserver/beets:latest
```

* Some images have helpers built in, these images are currently:
* [Openvscode-server](https://github.com/linuxserver/docker-openvscode-server/pull/10/files)
* [Code-server](https://github.com/linuxserver/docker-code-server/pull/95)
### docker compose

```yaml
---
services:
beets:
image: lscr.io/linuxserver/beets:latest
container_name: beets
environment:
DOCKER_MODS: ghcr.io/linuxserver/mods:beets-httpshell
PUID: 1000
PGID: 1000
TZ: Europe/London
HTTPSHELL_PORT: 5555
volumes:
- /path/to/config:/config
- /path/to/music:/music
- /path/to/downloads:/downloads
ports:
- 8337:8337
- 5555:5555
restart: unless-stopped
```

## Environment Variables

| Variable | Default | Description |
|---|---|---|
| `BEET_CMD` | `/lsiopy/bin/beet` | Path to the `beet` binary |
| `BEET_CONFIG` | `/config/config.yaml` | Path to the beets config file |
| `HTTPSHELL_PORT` | `5555` | Port the HTTP server listens on |
| `HTTPSHELL_BLOCKING_TIMEOUT` | `30` | Seconds to wait for the lock in `block` mode before the job is queued |

## API Usage

### Execute a command

```
POST /<command>
Content-Type: application/json

["arg1", "arg2", ...]
```

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.

**Response** (200 OK):

```json
{
"command": "stats",
"args": [],
"exit_code": 0,
"stdout": "Tracks: 1234\nTotal time: 3.2 days\n...",
"stderr": ""
}
```

### Health check

```
GET /health
```

Returns `200 OK` with server status:

```json
{
"status": "ok",
"default_mode": "parallel",
"queue_size": 0
}
```

### Examples

```bash
# Get library stats (default parallel mode)
curl -X POST http://localhost:5555/stats

# List all tracks by an artist
curl -X POST http://localhost:5555/list \
-H "Content-Type: application/json" \
-d '["artist:Radiohead"]'

# Import music in parallel (returns result when done, runs in parallel with other requests)
curl -X POST http://localhost:5555/import \
-H "Content-Type: application/json" \
-d '["/downloads/music", "--quiet", "--incremental"]'

# Queue an import (returns 202 immediately, runs in background)
curl -X POST 'http://localhost:5555/import?mode=queue' \
-H "Content-Type: application/json" \
-d '["/downloads/music"]'

# Update the library
curl -X POST http://localhost:5555/update

# Get beets configuration
curl -X POST http://localhost:5555/config

# Remove tracks matching a query (force, delete files)
curl -X POST http://localhost:5555/remove \
-H "Content-Type: application/json" \
-d '["artist:test", "-d", "-f"]'

# Move items to a new directory
curl -X POST http://localhost:5555/move \
-H "Content-Type: application/json" \
-d '["artist:Radiohead", "-d", "/music/favorites"]'

# Health check
curl http://localhost:5555/health
```

## Execution Modes

The execution mode is controlled per-request via the `?mode=` query parameter. If omitted, defaults to `parallel`.

### `parallel` (default)

Each request runs its command immediately in its own thread. Multiple commands execute in parallel. The response is returned when the command finishes.

```
Request 1 ──▶ [runs command] ──▶ 200 response
Request 2 ──▶ [runs command] ──▶ 200 response (runs in parallel)
```

### `block`

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.

```
Request 1 ──▶ [acquires lock, runs command] ──▶ 200 response
Request 2 ──▶ [waits for lock... acquired] ──▶ 200 response
Request 3 ──▶ [waits for lock... timeout] ──▶ 202 (queued)
```

### `queue`

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`).

```
Request 1 ──▶ 202 (queued, position 1)
Request 2 ──▶ 202 (queued, position 2)
[worker runs command 1, then command 2]
```

**202 Response:**

```json
{
"status": "queued",
"command": "import",
"args": ["/downloads/album"],
"queue_size": 1
}
```

## Lidarr Integration

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.

Create the script at a path accessible to Lidarr (e.g., `/config/scripts/beets-import.sh`):

```bash
#!/usr/bin/env bash

if [ -z "$lidarr_sourcepath" ]; then
echo "Error: lidarr_sourcepath environment variable not set"
exit 1
fi

curl -X POST --fail-with-body \
-H "Content-Type: application/json" \
-d "[\"$lidarr_sourcepath\"]" \
'http://beets:5555/import?mode=block'

if [ $? -ne 0 ]; then
echo "Import request failed"
exit 1
fi
```

> **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.

## Mod Structure

```text
root/
├── usr/local/bin/
│ └── beets-httpshell.py # HTTP server script
└── etc/s6-overlay/s6-rc.d/
├── init-mod-beets-httpshell/ # oneshot init (startup banner, env validation)
├── svc-mod-beets-httpshell/ # longrun service (HTTP server)
├── init-mods-end/dependencies.d/
│ └── init-mod-beets-httpshell
└── user/contents.d/
├── init-mod-beets-httpshell
└── svc-mod-beets-httpshell
```
5 changes: 5 additions & 0 deletions root/etc/s6-overlay/s6-rc.d/init-mod-beets-httpshell/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/with-contenv bash
Comment thread
aptalca marked this conversation as resolved.
Outdated

echo "**** installing beets-httpshell mod ****"
echo "**** httpshell port: ${HTTPSHELL_PORT:-5555} ****"
echo "**** beets-httpshell mod installed ****"
1 change: 1 addition & 0 deletions root/etc/s6-overlay/s6-rc.d/init-mod-beets-httpshell/type
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
oneshot
1 change: 1 addition & 0 deletions root/etc/s6-overlay/s6-rc.d/init-mod-beets-httpshell/up
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-mod-beets-httpshell/run

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

3 changes: 3 additions & 0 deletions root/etc/s6-overlay/s6-rc.d/svc-mod-beets-httpshell/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bash

exec s6-setuidgid abc python3 /usr/local/bin/beets-httpshell.py
1 change: 1 addition & 0 deletions root/etc/s6-overlay/s6-rc.d/svc-mod-beets-httpshell/type
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
longrun
7 changes: 0 additions & 7 deletions root/etc/s6-overlay/s6-rc.d/svc-mod-imagename-modname/run

This file was deleted.

Loading