Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
python-version: "3.12"

- name: Install dependencies
run: pip install -r requirements-test.txt
run: pip install -e ".[dev]"

- name: Run tests with coverage
run: pytest tests/ --cov=. --cov-report=xml --cov-report=term -v
run: pytest tests/ --cov=src/slskd_transform --cov-report=xml --cov-report=term -v

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# User config (contains API keys)
config.yml

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -158,3 +161,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.omc/
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM python:3.12-slim AS builder

WORKDIR /app
COPY pyproject.toml .
COPY src/ src/

RUN pip install --no-cache-dir --prefix=/install .

FROM python:3.12-slim

WORKDIR /app
COPY --from=builder /install /usr/local

RUN useradd --create-home appuser
USER appuser

RUN mkdir -p /app/music /app/downloads /app/organized
Comment on lines +14 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Permission denied—appuser cannot create directories under root-owned /app.

WORKDIR /app (line 11) creates /app owned by root. After switching to appuser (line 15), the RUN mkdir (line 17) fails because appuser lacks write permissions to /app.

🐛 Proposed fix: create directories before switching user
 WORKDIR /app
 COPY --from=builder /install /usr/local

+RUN mkdir -p /app/music /app/downloads /app/organized
 RUN useradd --create-home appuser
+RUN chown -R appuser:appuser /app
 USER appuser

-RUN mkdir -p /app/music /app/downloads /app/organized

 ENTRYPOINT ["slskd-transform"]
 CMD ["search"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RUN useradd --create-home appuser
USER appuser
RUN mkdir -p /app/music /app/downloads /app/organized
WORKDIR /app
COPY --from=builder /install /usr/local
RUN mkdir -p /app/music /app/downloads /app/organized
RUN useradd --create-home appuser
RUN chown -R appuser:appuser /app
USER appuser
ENTRYPOINT ["slskd-transform"]
CMD ["search"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 14 - 17, The mkdir step fails because WORKDIR /app
is root-owned and the Dockerfile switches to USER appuser before creating
directories; either create the dirs while still root or change ownership of /app
to appuser. Move the RUN mkdir -p /app/music /app/downloads /app/organized to
before USER appuser (i.e., run as root after WORKDIR /app), or add a root RUN
chown -R appuser:appuser /app before switching to USER appuser so appuser can
create/write the directories.


ENTRYPOINT ["slskd-transform"]
CMD ["search"]
186 changes: 132 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,94 +8,179 @@

<p align="center">
<a href="https://github.com/GeiserX/slskd-transform/blob/main/LICENSE"><img src="https://img.shields.io/github/license/GeiserX/slskd-transform?style=flat-square&color=FF6F00" alt="License"></a>
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.8%2B-blue?style=flat-square&logo=python&logoColor=white" alt="Python 3.8+"></a>
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square&logo=python&logoColor=white" alt="Python 3.10+"></a>
<a href="https://github.com/slskd/slskd"><img src="https://img.shields.io/badge/requires-slskd-1A1A2E?style=flat-square" alt="Requires slskd"></a>
<a href="https://github.com/GeiserX/slskd-transform/stargazers"><img src="https://img.shields.io/github/stars/GeiserX/slskd-transform?style=flat-square&color=FFD54F" alt="Stars"></a>
<a href="https://codecov.io/gh/GeiserX/slskd-transform"><img src="https://img.shields.io/codecov/c/github/GeiserX/slskd-transform?style=flat-square" alt="Coverage"></a>
</p>

---

**slskd-transform** is a Python tool that scans your local music library, searches the [Soulseek](https://www.slsknet.org/) network through [slskd](https://github.com/slskd/slskd) for matching FLAC versions of each track, and automatically enqueues them for download. It matches songs by **audio duration** rather than filenames alone, ensuring you get the correct track every time. Songs that cannot be found are reported in a CSV file for manual follow-up.
**slskd-transform** scans your local music library, searches the [Soulseek](https://www.slsknet.org/) network through [slskd](https://github.com/slskd/slskd) for matching FLAC versions of each track, and automatically enqueues them for download. It matches songs by **audio duration** rather than filenames alone, ensuring you get the correct track every time.

A companion script handles post-download organization, renaming all downloaded FLACs into a clean `Artist - Title.flac` structure using embedded metadata.
A companion `rename` command handles post-download organization, renaming all downloaded FLACs into a clean `Artist - Title.flac` structure using embedded metadata.

## Features

- **Duration-based matching** -- Compares local track duration against search results with a configurable tolerance (default: 15 seconds), avoiding mismatches from inconsistent naming.
- **Multi-threaded search** -- Distributes searches across multiple threads (default: 5) for faster processing of large libraries.
- **Duration-based matching** -- Compares local track duration against search results with a configurable tolerance (default: 15 seconds).
- **Recursive scanning** -- Point it at your existing music library with `--recursive`, no need to flatten files first.
- **Multi-threaded search** -- Distributes searches across multiple threads (default: 5) for faster processing.
- **Flexible configuration** -- Config file, environment variables, or CLI flags. No code editing required.
- **Automatic enqueue** -- Matched FLAC files are enqueued for download directly through the slskd API.
- **CSV reporting** -- Tracks that could not be found are written to `unfound_songs.csv` for later review.
- **Metadata-based renaming** -- The `rename-files.py` script reads FLAC tags and renames files to `Artist - Title.flac`, sanitizing any invalid characters.
- **CSV reporting** -- Tracks that could not be found are written to `unfound_songs.csv`.
- **Metadata-based renaming** -- Reads FLAC tags and renames files to `Artist - Title.flac`.
- **Docker support** -- Run alongside slskd in the same compose stack.

## Prerequisites

- **Python 3.8+**
- **[slskd](https://github.com/slskd/slskd)** running and accessible (by default at `http://127.0.0.1:5030`)
- **Python 3.10+**
- **[slskd](https://github.com/slskd/slskd)** running and accessible
- A valid slskd **API key** (configured in slskd's settings)
- A local directory containing the lossy music files you want to upgrade

## Installation

```bash
pip install git+https://github.com/GeiserX/slskd-transform.git
```

Or for development:

```bash
git clone https://github.com/GeiserX/slskd-transform.git
cd slskd-transform
pip install -r requirements.txt
pip install -e ".[dev]"
```

## Usage
## Quick Start

### Step 1 -- Search and enqueue FLAC downloads
```bash
# Set your API key (or put it in config.yml)
export SLSKD_API_KEY="your-api-key-here"

# Search for FLAC versions of all files in ./music
slskd-transform search

1. Place your lossy music files (MP3, AAC, OGG, etc.) in a `music/` directory inside the project root.
2. Open `main.py` and set your slskd connection details:
# Search recursively in your existing library
slskd-transform search --music-dir /path/to/library --recursive

```python
slskd = slskd_api.SlskdClient(
host="http://127.0.0.1:5030",
api_key="YOUR_API_KEY",
verify_ssl=False
)
# Rename downloaded FLACs using metadata
slskd-transform rename --source-dir /path/to/downloads --dest-dir /path/to/organized
```

3. Run the search:
## Configuration

slskd-transform loads configuration from multiple sources with this priority:

```bash
python main.py
```
CLI flags > Environment variables > Config file > Defaults
```
Comment on lines +74 to 76
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifiers to fenced code blocks.

Line 74, Line 121, Line 138, and Line 147 use unlabeled fenced blocks (MD040). Add a language (for example text, bash, or yaml) to satisfy markdownlint.

Also applies to: 121-123, 138-139, 147-148

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 74-74: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 74 - 76, The README contains unlabeled fenced code
blocks (e.g., the block showing "CLI flags  >  Environment variables  >  Config
file  >  Defaults" and other blocks at the referenced lines); add appropriate
language identifiers after the opening backticks (for example use ```text for
plain text tables, ```bash for shell examples, or ```yaml for YAML snippets) for
each fenced block at lines 74, 121-123, 138-139, and 147-148 so markdownlint
MD040 is satisfied and the blocks are properly highlighted.


The script will search Soulseek for a FLAC version of each local track, match by duration, and enqueue any matches for download. Any songs that could not be found will be saved to `unfound_songs.csv`.
### Config File

### Step 2 -- Rename downloaded FLACs
Create `config.yml` in your working directory or `~/.config/slskd-transform/config.yml`:

Once downloads are complete, use the renaming script to organize them:
```yaml
# slskd connection
host: "http://127.0.0.1:5030"
api_key: "your-api-key"
verify_ssl: false

1. Open `rename-files.py` and set the source and destination directories:
# Search settings
music_dir: "./music"
duration_tolerance: 15
num_threads: 5
search_timeout: 60
format: "flac"
recursive: false

```python
source_directory = '/path/to/slskd/downloads'
destination_directory = '/path/to/organized/music'
# Rename settings
source_dir: "./downloads"
destination_dir: "./organized"
```

2. Run the script:
### Environment Variables

All settings can be configured via `SLSKD_` prefixed environment variables:

| Variable | Description | Default |
|----------|-------------|---------|
| `SLSKD_HOST` | slskd instance URL | `http://127.0.0.1:5030` |
| `SLSKD_API_KEY` | slskd API key | -- |
| `SLSKD_VERIFY_SSL` | Enable SSL verification | `false` |
| `SLSKD_MUSIC_DIR` | Source directory with lossy files | `./music` |
| `SLSKD_DURATION_TOLERANCE` | Max duration difference (seconds) | `15` |
| `SLSKD_NUM_THREADS` | Concurrent search threads | `5` |
| `SLSKD_SEARCH_TIMEOUT` | Wait time for search results (seconds) | `60` |
| `SLSKD_FORMAT` | Target format to search for | `flac` |
| `SLSKD_RECURSIVE` | Scan directories recursively | `false` |
| `SLSKD_SOURCE_DIR` | Download directory for rename | `./downloads` |
| `SLSKD_DESTINATION_DIR` | Output directory for rename | `./organized` |

### CLI Reference

```bash
python rename-files.py
```
slskd-transform [OPTIONS] COMMAND [ARGS]...

Options:
-c, --config PATH Path to config.yml
--host TEXT slskd host URL
--api-key TEXT slskd API key
--no-verify-ssl Disable SSL verification
-t, --threads INTEGER Number of search threads
--help Show help

Commands:
search Search Soulseek for lossless versions and enqueue downloads
rename Rename downloaded FLACs using metadata
```

All `.flac` files in the source directory (including subdirectories) will be renamed to `Artist - Title.flac` and moved to the destination.
**search options:**
```
-m, --music-dir PATH Directory with lossy source files
-r, --recursive Scan music directory recursively
-f, --format TEXT Target format (default: flac)
--tolerance INTEGER Duration match tolerance in seconds
--timeout INTEGER Seconds to wait for search results
```

## Configuration
**rename options:**
```
-s, --source-dir PATH Directory where slskd downloads land
-d, --dest-dir PATH Destination for renamed files
```

| Parameter | Location | Default | Description |
|---|---|---|---|
| `host` | `main.py` | `http://127.0.0.1:5030` | slskd instance URL |
| `api_key` | `main.py` | -- | Your slskd API key |
| `MUSIC_DIR` | `main.py` | `./music` | Directory containing lossy source files |
| `duration_tolerance` | `main.py` | `15` (seconds) | Maximum duration difference for a match |
| `num_threads` | `main.py` | `5` | Number of concurrent search threads |
| `source_directory` | `rename-files.py` | -- | Where slskd downloads land |
| `destination_directory` | `rename-files.py` | -- | Where renamed FLACs are moved |
## Docker

```bash
docker run --rm \
-e SLSKD_HOST=http://slskd:5030 \
-e SLSKD_API_KEY=your-key \
-v /path/to/lossy:/app/music:ro \
-v /path/to/downloads:/app/downloads \
ghcr.io/geiserx/slskd-transform:2.0.0 search --recursive
```

Or in a compose stack alongside slskd:

```yaml
services:
slskd:
image: slskd/slskd:0.21.4
ports:
- "5030:5030"
volumes:
- ./slskd-data:/app

slskd-transform:
image: ghcr.io/geiserx/slskd-transform:2.0.0
environment:
SLSKD_HOST: http://slskd:5030
SLSKD_API_KEY: your-key
volumes:
- /path/to/lossy:/app/music:ro
- ./slskd-data/downloads:/app/downloads
command: ["search", "--recursive"]
```

## How It Works

Expand All @@ -110,16 +195,10 @@ Local library Soulseek network Your disk
│ │ │
└──── no match ──> unfound_songs.csv │
v
rename-files.py
slskd-transform rename
Artist - Title.flac
```

1. **Scan** -- `main.py` reads every file in the `music/` directory using [mutagen](https://mutagen.readthedocs.io/) to extract the audio duration.
2. **Search** -- For each track, a Soulseek search is issued via the slskd API with the query `"<song name> flac"`. Searches run in parallel across multiple threads with staggered starts to avoid flooding the network.
3. **Match** -- Each search result is compared by duration. The first result within the tolerance window is selected.
4. **Enqueue** -- Matched files are enqueued for download through slskd. Failed or unmatched tracks are logged.
5. **Rename** -- After downloading, `rename-files.py` walks the download directory, reads FLAC metadata, and moves files into a flat structure with clean filenames.

## Related Music Tools

| Project | Description |
Expand All @@ -128,7 +207,6 @@ Local library Soulseek network Your disk
| [audio-transcode-watcher](https://github.com/GeiserX/audio-transcode-watcher) | Automated multi-format audio transcoding with lyrics fetching |
| [jellyfin-encoder](https://github.com/GeiserX/jellyfin-encoder) | Automatic 720p HEVC/AV1 transcoding for Jellyfin |


## License

This project is licensed under the [MIT License](LICENSE).
This project is licensed under the [GPL-3.0 License](LICENSE).
19 changes: 19 additions & 0 deletions config.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# slskd-transform configuration
# All fields are optional — defaults shown below

# slskd connection
host: "http://127.0.0.1:5030"
api_key: "" # REQUIRED (or set SLSKD_API_KEY env var)
verify_ssl: false

# Search settings
music_dir: "./music" # Where your lossy source files live
duration_tolerance: 15 # Seconds of acceptable duration difference
num_threads: 5 # Concurrent search threads
search_timeout: 60 # Seconds to wait for search results
format: "flac" # Target format (appended to search query)
recursive: false # Scan music_dir recursively

# Rename settings
source_dir: "./downloads" # Where slskd puts completed downloads
destination_dir: "./organized" # Where renamed FLACs are moved to
Loading
Loading