Skip to content
Closed
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
20 changes: 20 additions & 0 deletions docker-compose.test-clients.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# - NZBHydra: http://localhost:5076 (no auth by default)
# - SABnzbd: http://localhost:8085 (complete setup wizard for API key)
# - rTorrent: http://localhost:8000 (admin / admin - if auth enabled)
# - slskd: http://localhost:5030 (slskd / slskdslskd; API key in .local/test-clients/slskd/config/slskd.yml)
#

services:
Expand All @@ -39,6 +40,7 @@ services:
# - NZBHydra: http://nzbhydra:5076
# - SABnzbd: http://sabnzbd:8080
# - rTorrent: http://rtorrent:80 (XMLRPC via HTTP) or rtorrent (port 5000 for SCGI)
# - slskd: http://slskd:5030 (downloads land in /downloads/slskd/complete)
ports:
- "8084:8084"
volumes:
Expand Down Expand Up @@ -68,6 +70,7 @@ services:
- transmission
- deluge
- rtorrent
- slskd
restart: unless-stopped

prowlarr:
Expand Down Expand Up @@ -198,3 +201,20 @@ services:
- 5076:5076
restart: unless-stopped

slskd:
image: slskd/slskd:latest
container_name: test-slskd
environment:
- TZ=UTC
# Bootstrap: write .local/test-clients/slskd/config/slskd.yml first (see
# scripts/test_clients.py for a template). It holds the Soulseek login, the
# API key Shelfmark uses, and the download directories below.
- SLSKD_CONFIG=/app/slskd.yml
volumes:
- ./.local/test-clients/slskd/config:/app
- ./.local/test-clients/slskd/shares:/shares:ro
- ./.local/test-clients/downloads:/downloads
ports:
- "5030:5030" # Web UI / API
- "50300:50300" # Soulseek peer listen port
restart: unless-stopped
94 changes: 94 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This document lists all configuration options that can be set via environment va
- [Advanced](#advanced)
- [Prowlarr](#prowlarr)
- [Newznab](#newznab)
- [Soulseek (slskd)](#soulseek-slskd)
- [AudiobookBay](#audiobookbay)
- [IRC](#irc)
- [Download Clients](#download-clients)
Expand Down Expand Up @@ -1397,6 +1398,99 @@ Automatically retry search without category filtering if no results are found

</details>

## Soulseek (slskd)

| Variable | Description | Type | Default |
|----------|-------------|------|---------|
| `SLSKD_ENABLED` | Enable searching and downloading books through slskd | boolean | `false` |
| `SLSKD_URL` | Base URL of the slskd web interface | string | _none_ |
| `SLSKD_API_KEY` | An API key from slskd's configuration (web.authentication.api_keys) with the readwrite role | string (secret) | _none_ |
| `SLSKD_DOWNLOAD_PATH` | Where slskd's completed downloads folder is mounted inside Shelfmark. Leave empty when both containers see it at the same path, or use Settings > Advanced > Remote Path Mappings with client 'slskd'. | string | _none_ |
| `SLSKD_SEARCH_TIMEOUT` | How long a Soulseek search collects peer responses. Longer waits find more peers but make every search slower. | number | `15` |
| `SLSKD_RESPONSE_LIMIT` | Stop a search early once this many peers have answered | number | `100` |
| `SLSKD_QUEUE_TIMEOUT_MINUTES` | Give up when the peer has not started sending within this long. Set to 0 to wait indefinitely. | number | `60` |
| `SLSKD_REMOVE_COMPLETED` | Clear the transfer from slskd's download list after Shelfmark imports it | boolean | `true` |

<details>
<summary>Detailed descriptions</summary>

#### `SLSKD_ENABLED`

**Enable Soulseek source**

Enable searching and downloading books through slskd

- **Type:** boolean
- **Default:** `false`

#### `SLSKD_URL`

**slskd URL**

Base URL of the slskd web interface

- **Type:** string
- **Default:** _none_

#### `SLSKD_API_KEY`

**API Key**

An API key from slskd's configuration (web.authentication.api_keys) with the readwrite role

- **Type:** string (secret)
- **Default:** _none_

#### `SLSKD_DOWNLOAD_PATH`

**Downloads Path**

Where slskd's completed downloads folder is mounted inside Shelfmark. Leave empty when both containers see it at the same path, or use Settings > Advanced > Remote Path Mappings with client 'slskd'.

- **Type:** string
- **Default:** _none_

#### `SLSKD_SEARCH_TIMEOUT`

**Search Wait (seconds)**

How long a Soulseek search collects peer responses. Longer waits find more peers but make every search slower.

- **Type:** number
- **Default:** `15`
- **Constraints:** min: 3, max: 120

#### `SLSKD_RESPONSE_LIMIT`

**Max Peer Responses**

Stop a search early once this many peers have answered

- **Type:** number
- **Default:** `100`
- **Constraints:** min: 10, max: 1000

#### `SLSKD_QUEUE_TIMEOUT_MINUTES`

**Queue Timeout (minutes)**

Give up when the peer has not started sending within this long. Set to 0 to wait indefinitely.

- **Type:** number
- **Default:** `60`
- **Constraints:** min: 0, max: 1440

#### `SLSKD_REMOVE_COMPLETED`

**Remove completed transfers from slskd**

Clear the transfer from slskd's download list after Shelfmark imports it

- **Type:** boolean
- **Default:** `true`

</details>

## AudiobookBay

| Variable | Description | Type | Default |
Expand Down
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Works great alongside the following library tools, with support for automatic im
## ✨ Features

- **One-Stop Interface** - A clean, modern UI to search, browse, and download from multiple configured sources in one place
- **Multiple Sources** - Configurable web, torrent, usenet, and IRC source support
- **Multiple Sources** - Configurable web, torrent, usenet, IRC, and Soulseek (slskd) source support
- **Audiobook Support** - Full audiobook search and download with dedicated processing
- **Flexible Search** - Search metadata providers (Hardcover, Open Library, Google Books) for rich book and audiobook discovery, or query configured sources directly
- **Multi-User & Requests** - Share your instance with others, let users browse and request books, and manage approvals with configurable notifications
Expand Down Expand Up @@ -149,6 +149,7 @@ Some of the additional options available in Settings:
- **Additional audiobook sources** - Configure additional sources for audiobook discovery
- **Direct Download mirrors** - Supply your own Anna's Archive mirror URLs; Auto mode tries them in the order listed. The `annas-archive.is` domain does not currently work as a source — use `annas-archive.gl` instead (checked August 2026; mirror availability changes)
- **IRC** - Add details for IRC book sources and download directly from the UI. Most networks serve audiobooks from the same channel as ebooks (on `irc.irchighway.net` that's `#ebooks`, while `#bookz` is effectively inactive), so leave the separate audiobook channel blank unless your network actually indexes one. IRC audiobooks usually arrive as ZIP/RAR archives — keep those enabled under Supported Audiobook Formats or the releases are filtered out of results
- **Soulseek (slskd)** - Search the Soulseek network and download through a [slskd](https://github.com/slskd/slskd) instance. Point Shelfmark at slskd's URL with an API key (`web.authentication.api_keys` in `slskd.yml`, role `readwrite`), and mount slskd's downloads folder into the Shelfmark container. If the folder sits at a different path inside Shelfmark, set **Downloads Path** in the slskd settings or add a Remote Path Mapping for client `slskd`. Ebook results are single files; audiobook results group a peer's folder of audio files into one release
- **Library Link** - Add a link to your Calibre-Web or Grimmory instance in the UI header
- **File processing** - Customiseable download paths, file renaming and directory creation with template-based renaming
- **Network Settings** - Custom proxy support (SOCKS5 + HTTP/S) and configurable DNS
Expand Down
122 changes: 122 additions & 0 deletions scripts/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
- Complete the setup wizard at http://localhost:8085
- API key will be auto-detected by this script
- In cwabd, copy API key from SABnzbd Config > General

slskd (Soulseek):
- Before the first `up`, write .local/test-clients/slskd/config/slskd.yml.
Run `uv run python scripts/test_clients.py --write-slskd-config` to create
one with a throwaway Soulseek login and a fresh API key, then create the
download folders it names (this script does both).
- In Shelfmark: Settings > Soulseek (slskd), URL http://slskd:5030 and the
API key from that file; Downloads Path /downloads/slskd/complete.
"""

import sys
Expand Down Expand Up @@ -84,8 +92,38 @@
"rtorrent": {
"url": "http://localhost:8000/RPC2",
},
# Soulseek
"slskd": {
"url": "http://localhost:5030",
"config_file": Path(".local/test-clients/slskd/config/slskd.yml"),
},
}

SLSKD_CONFIG_TEMPLATE = """# slskd test-stack config (generated by scripts/test_clients.py; not committed).
# Soulseek creates the account on first login, so any unused username works.
web:
port: 5030
authentication:
disabled: false
username: slskd
password: slskdslskd
api_keys:
shelfmark:
key: {api_key}
role: readwrite
cidr: 0.0.0.0/0,::/0
soulseek:
username: {soulseek_username}
password: {soulseek_password}
description: shelfmark test-stack client
directories:
downloads: /downloads/slskd/complete
incomplete: /downloads/slskd/incomplete
shares:
directories:
- /shares
"""

# Test magnet link (Ubuntu ISO - legal, small metadata)
TEST_MAGNET = "magnet:?xt=urn:btih:3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0&dn=ubuntu-22.04.3-live-server-amd64.iso"

Expand Down Expand Up @@ -522,7 +560,85 @@ def test_rtorrent() -> bool:
return False


def _read_slskd_api_key() -> str | None:
"""Pull the shelfmark API key out of the generated slskd.yml (no YAML dependency)."""
config_file = CONFIG["slskd"]["config_file"]
if not config_file.exists():
return None
for line in config_file.read_text().splitlines():
stripped = line.strip()
if stripped.startswith("key:"):
return stripped.split(":", 1)[1].strip()
return None


def write_slskd_config() -> Path:
"""Write a fresh slskd.yml with a throwaway Soulseek login and API key."""
import secrets

config_file = CONFIG["slskd"]["config_file"]
config_file.parent.mkdir(parents=True, exist_ok=True)
suffix = secrets.token_hex(2)
config_file.write_text(
SLSKD_CONFIG_TEMPLATE.format(
api_key=secrets.token_hex(24),
soulseek_username=f"shelfmark-dev-{suffix}",
soulseek_password=secrets.token_urlsafe(12),
)
)
for folder in (
"shares/TestBooks",
"../downloads/slskd/complete",
"../downloads/slskd/incomplete",
):
(config_file.parent.parent / folder).resolve().mkdir(parents=True, exist_ok=True)
return config_file


def test_slskd() -> bool:
"""Test slskd connection and Soulseek login state."""
print("\n" + "=" * 50)
print("Testing slskd (Soulseek)")
print("=" * 50)

# The slskd client needs the app's log/config dirs; keep them out of the way.
import os
import tempfile

scratch = tempfile.mkdtemp(prefix="shelfmark-test-clients-")
for var, sub in (("LOG_ROOT", ""), ("CONFIG_DIR", "config"), ("TMP_DIR", "tmp")):
os.environ.setdefault(var, str(Path(scratch, sub)))
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from shelfmark.release_sources.slskd.api import SlskdClient

api_key = _read_slskd_api_key()
if not api_key:
print(f" ERROR: no API key found in {CONFIG['slskd']['config_file']}")
print(" Run: uv run python scripts/test_clients.py --write-slskd-config")
return False

client = SlskdClient(CONFIG["slskd"]["url"], api_key, timeout=10)
success, message = client.test_connection()
print(f" {message}")
if not success:
print(" Is the container running? docker ps | grep slskd")
return False

print(f" Downloads directory (in slskd): {client.get_download_directory()}")
search_id = client.start_search(
"shelfmark-connectivity-probe", timeout_ms=1000, response_limit=1
)
print(f" Search API ok (id {search_id[:8]}...)")
client.delete_search(search_id)
print(" SUCCESS: slskd is working!")
return True


def main() -> int:
if "--write-slskd-config" in sys.argv:
print(f"Wrote {write_slskd_config()}")
return 0

print("Download Client Test Suite")
print("=" * 50)
print("Make sure containers are running:")
Expand All @@ -546,6 +662,12 @@ def main() -> int:
results["deluge"] = test_deluge()
results["rtorrent"] = test_rtorrent()

# Test Soulseek
print("\n" + "=" * 50)
print("SOULSEEK")
print("=" * 50)
results["slskd"] = test_slskd()

# Summary
print("\n" + "=" * 50)
print("SUMMARY")
Expand Down
1 change: 1 addition & 0 deletions shelfmark/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1923,6 +1923,7 @@ def advanced_settings() -> list[SettingsField]:
{"value": "rtorrent", "label": "rTorrent"},
{"value": "nzbget", "label": "NZBGet"},
{"value": "sabnzbd", "label": "SABnzbd"},
{"value": "slskd", "label": "slskd"},
],
"defaultValue": "qbittorrent",
},
Expand Down
2 changes: 2 additions & 0 deletions shelfmark/release_sources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ReleaseProtocol(StrEnum):
TORRENT = "torrent" # BitTorrent
NZB = "nzb" # Usenet NZB
DCC = "dcc" # IRC DCC
SOULSEEK = "soulseek" # Soulseek peer transfer (via slskd)


class SourceUnavailableError(Exception):
Expand Down Expand Up @@ -433,6 +434,7 @@ def cancel(self, task_id: str) -> bool:
"shelfmark.release_sources.irc",
"shelfmark.release_sources.newznab",
"shelfmark.release_sources.prowlarr",
"shelfmark.release_sources.slskd",
)
_builtin_source_state = {"loaded": False}

Expand Down
22 changes: 22 additions & 0 deletions shelfmark/release_sources/slskd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
slskd release source plugin.

Searches the Soulseek network through a slskd instance and downloads the chosen
files with it. Unlike torrent/usenet sources, search and download are the same
service: a result is a file on a specific peer, and only slskd can fetch it.

Includes:
- SlskdSource: Search integration
- SlskdHandler: Download handling via slskd transfers
"""

# Import submodules to trigger decorator registration
from shelfmark.release_sources.slskd import (
handler as handler,
)
from shelfmark.release_sources.slskd import (
settings as settings,
)
from shelfmark.release_sources.slskd import (
source as source,
)
Loading
Loading