Skip to content
Open

1.0 #68

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
e21718f
refactor(config): @ConfigProperty + runtime overrides via DB-backed p…
skjaere Feb 28, 2026
77616df
feat(nntp): multi-pool support with runtime add/remove + priority fal…
skjaere Feb 28, 2026
2564479
feat(auth): JWT authentication for API + temporary file access tokens
skjaere Mar 1, 2026
076e73b
feat(nzb): async import queue + paginated history API
skjaere Mar 1, 2026
c26c955
fix(reliability): glitchtip-reported bugs + Premiumize 200-with-error
skjaere Mar 7, 2026
5deb0b9
fix(config): sync pools/timeouts after runtime overrides apply
skjaere Mar 7, 2026
b4e179a
fix(queue): lazy file-path resolution in queue responses
skjaere Mar 8, 2026
d7a1600
feat(health): torrent health check + repair pipeline
skjaere Mar 8, 2026
0af9563
fix(realdebrid): link handling + sync improvements + CLAUDE.md docs
skjaere Mar 14, 2026
c96ef53
feat(webdav): move WebDAV API under /webdav prefix
skjaere Apr 11, 2026
5f1bfea
build: bundle frontend into Spring Boot JAR via git submodule
skjaere Apr 11, 2026
b521f6f
docs(example): overhaul docker-compose example + monitoring stack
skjaere Apr 12, 2026
fe95d59
feat(rclone): push VFS cache invalidation on file changes
skjaere Apr 12, 2026
e12f987
feat(ui): overview cards + per-stream summary + Micrometer migration
skjaere Apr 18, 2026
227b44c
fix(providers): don't fail startup when RD/Premiumize aren't configured
skjaere Apr 19, 2026
ef274fa
fix(auth): fail fast on JWT secrets shorter than 32 bytes
skjaere Apr 19, 2026
0d6527d
fix(pgmq): cap poison-message retries + dead-letter queue for every c…
skjaere Apr 19, 2026
ec5dc7c
build: bump debridav-frontend to 46fc5cd
skjaere Apr 19, 2026
f26c213
feat(dashboard): discover Grafana dashboards from the `debridav` folder
skjaere Apr 19, 2026
8995184
build: bump debridav-frontend to e7ebba2
skjaere Apr 19, 2026
a884a2b
fix(dashboard): drop suspend from /api/v1/grafana/dashboards
skjaere Apr 19, 2026
1aa8f21
feat(ui): Logs page + health-check consolidation + SPA deep links
skjaere Apr 19, 2026
69f42e9
build: bump nzb-streamer to v0.8.0
skjaere Apr 19, 2026
8f6daaa
chore: clear detekt violations blocking the build
skjaere Apr 25, 2026
c27fde2
fix(sabnzbd): route to async import only when NNTP is configured
skjaere Apr 25, 2026
5cb2497
fix(jpa): cascade-delete NzbContents when its NzbDocument is removed
skjaere Apr 25, 2026
496cf03
fix(realdebrid): refresh stale download when cached URL is dead
skjaere Apr 25, 2026
979a33d
refactor(jpa): tighten transactional and fetch boundaries
skjaere Apr 25, 2026
0767023
refactor(jpa): cap unbounded queries, narrow cascades, index FK columns
skjaere Apr 25, 2026
54e9666
build: bump debridav-frontend to 66375d1
skjaere Apr 25, 2026
ce6f4ff
feat(health): emit metrics from check + repair handlers
skjaere Apr 25, 2026
1c40659
feat(pgmq): publish queue-depth gauges for dashboards
skjaere Apr 25, 2026
b44d42a
feat(pgmq): age out stale DLQ messages into the archive sweep
skjaere Apr 25, 2026
8da3596
build: include yenc_kotlin_wrapper as composite build for multi-arch
skjaere Apr 25, 2026
c8b752e
fix(metrics): bind Resilience4j rate limiters to Micrometer
skjaere Apr 25, 2026
d1dbdfc
build: drop yenc_kotlin_wrapper composite-build override
skjaere Apr 25, 2026
6e7b744
build: bump nzb-streamer to v0.9.1
skjaere Apr 25, 2026
4658477
chore: wrap two long lines so detekt passes
skjaere Apr 25, 2026
18734bb
fix(metrics): count NZB files in library breakdown
skjaere Apr 25, 2026
47b9115
fix(realdebrid): include error body in unrestrict warnings
skjaere Apr 25, 2026
1d7ba63
fix(streaming): watchdog closes output when no bytes flow for 5 min
skjaere Apr 25, 2026
6d4dd46
perf(jpa): disable Open Session In View
skjaere Apr 25, 2026
3a99630
test(content): update ContentIT for /webdav URL prefix
skjaere Apr 25, 2026
6f10c28
docs(example): sync Grafana dashboards with the k8s configmap
skjaere Apr 25, 2026
266e9aa
refactor: drop pointless Dispatchers.IO offloads
skjaere Apr 26, 2026
a26ebfe
chore: replace inline FQCNs with imports where they don't collide
skjaere Apr 26, 2026
56ef0d8
fix(jpa): add @Transactional to derived delete methods
skjaere Apr 26, 2026
eef96d6
docs(readme): rewrite for 1.0
skjaere Apr 26, 2026
6b3c76f
build: bump nzb-streamer to v0.9.0
skjaere Apr 26, 2026
612ce3a
perf(torrent-add): batch directory creation and JDBC writes
skjaere Apr 26, 2026
e9503da
Merge pull request 'perf(torrent-add): batch directory creation and J…
Apr 26, 2026
a3b3267
chore: remove legacy Dockerfile + dev/ directory
skjaere Apr 26, 2026
5658db8
docs(readme): document how to access the UI
skjaere Apr 26, 2026
1c62020
docs(readme): document rclone VFS cache invalidation
skjaere Apr 26, 2026
f3494aa
docs(readme): promote Health checking & repair to its own section
skjaere Apr 26, 2026
fb53448
chore: untrack example_old/
skjaere Apr 26, 2026
c3ed0f5
chore: untrack mcp-config.json
skjaere Apr 26, 2026
0bf08cd
chore: delete stale version.properties
skjaere Apr 26, 2026
5a52491
fix(realdebrid): surface real HTTP errors from /torrents/info
skjaere Apr 26, 2026
c4d5439
docs(readme): drop bogus migration items for 1.0-only features
skjaere Apr 26, 2026
da0e58f
docs(readme): drop Elfhosted section
skjaere Apr 26, 2026
fdd68af
fix(jpa): stop pinning Hikari connections across suspending HTTP calls
skjaere Apr 25, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true

- uses: actions/setup-java@v4
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true

- uses: actions/setup-java@v4
with:
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.gradle/
build/
codecov*
codecov*
example_old/
mcp-config.json
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "debridav-frontend"]
path = debridav-frontend
url = https://github.com/skjaere/debridav-frontend.git
279 changes: 279 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build and Test Commands

```bash
# Build
./gradlew build # Full build with tests
./gradlew bootJar # Build Spring Boot JAR
./gradlew bootRun # Run application directly
./gradlew jibDockerBuild # Build Docker image locally

# Test
./gradlew test # Run all tests
./gradlew test --tests "io.skjaere.debridav.test.SomeTest" # Run single test class
./gradlew test --tests "*SomeTest.testMethod" # Run single test method

# Other
./gradlew compileKotlin # Compile only (no tests)
```

## Technology Stack

- **Kotlin 2.3.0** with **Java 25** (virtual threads via Loom)
- **Spring Boot 4.0.0** with Spring Data JPA
- **PostgreSQL** with Flyway migrations
- **Ktor 3.3.3** for HTTP client operations
- **Milton 4.0.4** for WebDAV protocol
- **Kotlin Coroutines** with custom `Dispatchers.LOOM` for virtual thread integration

## Architecture Overview

DebriDAV creates a WebDAV-mountable virtual filesystem backed by debrid service providers. It emulates the qBittorrent and SABnzbd APIs for integration with Sonarr/Radarr.

### Core Modules

| Package | Purpose |
|---------|---------|
| `debrid/client/` | Provider implementations (RealDebrid, Premiumize, Easynews, TorBox) with abstract `DebridClient` |
| `fs/` | Virtual filesystem layer - `DatabaseFileService` manages file hierarchy using PostgreSQL LTree |
| `resource/` | WebDAV resource factory connecting Milton to the virtual FS |
| `torrent/` | qBittorrent API emulation (`QBittorrentEmulationController`) |
| `nntp/` | Usenet/NZB support with streaming RAR parsing and Yenc decompression |
| `archive/` | RAR file parsing (`Rar4Parser`) for metadata extraction |
| `cache/` | Byte-range caching for metadata extraction (`FileChunkCachingService`) |
| `arrs/` | Sonarr/Radarr integration services |

### Data Flow

1. Sonarr/Radarr send requests to qBittorrent-emulated API
2. Torrent/NZB content checked against debrid provider caches
3. Cached content registered in PostgreSQL as virtual files
4. WebDAV server exposes virtual filesystem for media server mounting
5. On file access, content streamed from debrid provider with chunk caching

### Database

- PostgreSQL required (uses LTree extension for hierarchical paths)
- Entities: `Torrent`, `UsenetEntity`, `DebridFileContents`, `FileChunk`
- Migrations in `src/main/resources/db/migration/` (V1-V11)

## Code Patterns

**Configuration**: Use `@ConfigurationProperties` classes in `DebridavConfiguration.kt`. Properties defined in `application.properties`.

**Async operations**: Use Kotlin coroutines with `Dispatchers.LOOM` for blocking I/O:
```kotlin
withContext(Dispatchers.LOOM) {
// blocking operation
}
```

**Transactions**: Use `TransactionTemplate` for explicit transaction boundaries in services.

**Testing**: Integration tests use TestContainers (PostgreSQL), MockServer for HTTP, MockK for mocking. Tests in `src/test/kotlin/io/skjaere/debridav/test/`.

## Real-Debrid Integration

### Key Files

| File | Purpose |
|------|---------|
| `debrid/client/realdebrid/RealDebridClient.kt` | Main client — cache checking, torrent management, link unrestriction |
| `debrid/client/realdebrid/support/RealDebridTorrentService.kt` | Torrent sync and DB persistence |
| `debrid/client/realdebrid/support/RealDebridDownloadService.kt` | Download sync and DB persistence |
| `debrid/client/realdebrid/RealDebridConfigurationProperties.kt` | Configuration properties (`real-debrid.*`) |
| `debrid/client/realdebrid/RealDebridConfiguration.kt` | Spring bean config including Resilience4j rate limiter |
| `debrid/client/realdebrid/RealDebridActuatorEndpoint.kt` | Actuator endpoint for toggling torrent import at runtime |

### Class Hierarchy

`RealDebridClient` extends `DebridCachedTorrentClient` and `DebridCachedContentClient`, implements `StreamableLinkPreparable` (delegated to `DefaultStreamableLinkPreparer`) and `ConfigurationTester`.

### API Endpoints Used

All calls go to `https://api.real-debrid.com/rest/1.0` (configurable). Authentication is via Bearer token (`real-debrid.api-key`).

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/torrents/addMagnet` | POST | Submit magnet link (form-encoded `magnet=...`) |
| `/torrents/` | GET | List user's torrents (paginated, 100/page) |
| `/torrents/info/{id}` | GET | Get torrent info with files and links |
| `/torrents/selectFiles/{id}` | POST | Select files from torrent (form-encoded `files=1,2,3`) |
| `/torrents/delete/{id}` | DELETE | Remove torrent from account |
| `/unrestrict/link` | POST | Convert RD share link → direct download URL |
| `/downloads` | GET | List user's downloads (paginated, 100/page) |
| `/downloads/delete/{id}` | DELETE | Remove download from account |
| `/user` | GET | Validate API key (used by `ConfigurationTester`) |

### End-to-End Flow

**Phase 1 — Torrent Addition:**
1. `getCachedFiles(magnet)` checks DB for existing torrent by info hash
2. If not found: `POST /torrents/addMagnet` → `GET /torrents/info/{id}` → save to `RealDebridTorrentEntity`

**Phase 2 — File Selection:**
1. `getIdsToSelect()` filters for video files (`.mp4`, `.mkv`, `.avi`, `.ts`)
2. `POST /torrents/selectFiles/{id}` with selected file IDs
3. `GET /torrents/info/{id}` to retrieve links for selected files
4. If no links available (not cached): DELETE torrent, return empty list

**Phase 3 — Link Unrestriction:**
1. For each file link, check DB for existing `RealDebridDownloadEntity`
2. If not found: `POST /unrestrict/link` → returns direct download URL, saved to DB
3. Returns `List<CachedFile>` with path, download URL, MIME type, and params (`torrentId`, `linkId`)

**Phase 4 — Streaming:**
1. `getStreamableLink()` looks up download by hash + filename + size in DB
2. `isLinkAlive()` — HEAD request to download URL (rate-limited, cached 5 min)
3. If alive: return URL. If dead: delete and fetch fresh link via unrestrict
4. `DefaultStreamableLinkPreparer` builds Ktor HTTP GET with byte-range headers for seeking support

### Rate Limiting

Resilience4j `RateLimiter`: **249 requests per 1 minute** (just under RD's ~250/min limit), 5-second timeout per acquisition.

### Scheduled Sync

`syncTorrentsTask()` runs on a configurable schedule (`real-debrid.sync-poll-rate`, default `PT24H`):
- Clears and re-fetches all `RealDebridTorrentEntity` records (paginated `/torrents/`)
- Clears and re-fetches all `RealDebridDownloadEntity` records (paginated `/downloads`)
- Can be toggled at runtime via the actuator endpoint

### Database Entities

**`RealDebridTorrentEntity`**: `torrentId` (indexed), `name`, `hash` (indexed), `links` (ElementCollection), `files` (one-to-many `TorrentsInfoFile`)

**`RealDebridDownloadEntity`**: `downloadId` (indexed), `filename`, `mimeType`, `fileSize`, `link` (RD share link), `host`, `download` (actual URL), `chunks`, `streamable`

**Key query**: `getDownloadByHashAndFilenameAndSize()` — native SQL joining downloads → torrent links → torrents to find a download by torrent hash + filename + file size.

### Error Handling

- `isCached()` always returns `true` (RD doesn't expose a cache-check API; availability is determined during file selection)
- HTTP 4xx → `DebridClientError`, 5xx → `DebridProviderError`
- `addMagnet` failures return `FailedAddMagnetResponse` with reason (not thrown)
- `unrestrict` failures logged as warnings, return `ErrorUnrestrictLinkResponse`
- Configurable retries in `DebridCachedContentService` (default 1, 200ms delay)

## NNTP/Usenet Integration

### External Artifacts

| Artifact | Version | Purpose |
|----------|---------|---------|
| `com.github.skjaere:nzb-streamer` | 0.7.0 | NZB parsing, NNTP article fetching, Yenc decompression, RAR/7zip archive parsing, and file streaming |
| `com.github.skjaere:mock-nntp-server` | 0.2.0 | Test-only mock NNTP server |

**nzb-streamer** internally depends on **ktor-nntp-client** (a Ktor-based NNTP protocol client) for connecting to Usenet servers, fetching articles by message-ID, and managing connection pools with TLS support and server priority failover.

### Key Files

| File | Purpose |
|------|---------|
| `usenet/NzbStreamerConfiguration.kt` | NNTP server pool config, creates `NzbStreamer` bean |
| `usenet/NzbImportService.kt` | Orchestrates NZB import: parse → extract metadata → register in filesystem |
| `usenet/NzbHealthCheckService.kt` | Scheduled verification that NZB segments still exist on Usenet |
| `usenet/sabnzbd/SabnzbdApiController.kt` | SABnzbd API emulation endpoints |
| `usenet/sabnzbd/SabNzbdService.kt` | NZB handling and SABnzbd response building |
| `usenet/pgmq/PgmqSpringConfiguration.kt` | PostgreSQL message queue setup (3 queues) |
| `usenet/pgmq/PgmqConsumer.kt` | Generic message consumer loop |
| `usenet/pgmq/NzbHealthCheckHandler.kt` | Processes health check messages |
| `usenet/pgmq/NzbHealthRepairHandler.kt` | Blocklists failed NZBs in Sonarr/Radarr |
| `usenet/nzb/NzbDocumentEntity.kt` | JPA entity storing parsed NZB metadata as JSONB |
| `usenet/UsenetDownload.kt` | JPA entity tracking download status |
| `usenet/queue/NzbImportRecord.kt` | JPA entity tracking import queue status |
| `resource/NzbFileResource.kt` | WebDAV resource for streaming NZB files via Milton |

### Configuration Properties

**`nntp.*`** (all conditional on `nntp.enabled=true`):
- `enabled` — enable/disable NNTP support
- `concurrency` (default 4) — concurrent NNTP streaming threads
- `forwardThresholdBytes` (default 102400) — byte threshold for forward seeking
- `healthCheckInterval` (default 7 days) — how often to reverify NZB segments
- `healthCheckPollRate` (default 5 min) — poll rate for health check scheduling
- `pools` — list of NNTP server pools, each with: `host`, `port`, `username`, `password`, `useTls`, `maxConnections`, `priority`

**`pgmq.*`**:
- `importConcurrency` (default 2) — workers processing NZB imports
- `importVisibilityTimeout` (default 10 min) — message lock duration
- `importPollInterval` (default 2 sec) — queue poll rate
- `healthCheckConcurrency` (default 1), `healthRepairConcurrency` (default 2)

### End-to-End Flow

**Phase 1 — NZB Upload (SABnzbd API emulation):**
1. Sonarr/Radarr POST NZB file to `/api?mode=addfile`
2. `SabNzbdService` creates `UsenetDownload` (QUEUED) and `NzbImportRecord` (QUEUED)
3. Sends `NzbImportMessage` (NZB bytes as Base64) to PGMQ `nzb_import` queue
4. Returns immediately to caller

**Phase 2 — Async Import (PGMQ consumer):**
1. `PgmqConsumer` picks up message from `nzb_import` queue
2. `NzbImportService.executeImport()`:
- Decodes NZB bytes, calls `nzbStreamer.prepare(nzbBytes)`
- nzb-streamer parses NZB XML, fetches initial articles from NNTP servers via ktor-nntp-client
- Yenc-decodes article bodies, parses RAR/7zip archive headers to extract file metadata
- Returns `PrepareResult`: `Success`, `MissingArticles`, `Failure`, or `UnsupportedArchive`
3. On success: `nzbStreamer.resolveStreamableFiles(metadata)` → list of files with volume/offset info
4. Creates `NzbDocumentEntity` (files + streamableFiles stored as JSONB), `NzbContents` per file, and `RemotelyCachedEntity` entries in the virtual filesystem
5. Updates `UsenetDownload.status` → COMPLETED

**Phase 3 — File Streaming (WebDAV access):**
1. Media server accesses file via WebDAV
2. `StreamableResourceFactory` creates `NzbFileResource` from `NzbContents` entity
3. `NzbFileResource.sendContent()` calls `nzbStreamer.streamFile(nzbDocument, streamableFile, range)`
4. nzb-streamer fetches NNTP articles on-demand, Yenc-decodes, reconstructs archive data, and streams the extracted file content via a `ByteReadChannel`
5. Supports byte-range requests for seeking/scrubbing

**Phase 4 — Health Check & Repair:**
1. `NzbHealthCheckService` runs on schedule, finds NZB documents not verified within `healthCheckInterval`
2. Sends `NzbHealthCheckMessage` to PGMQ `nzb_health_check` queue
3. `NzbHealthCheckHandler` calls `nzbStreamer.verifySegments()` to check article availability
4. If articles missing: sends `NzbHealthRepairMessage` to `nzb_health_repair` queue
5. `NzbHealthRepairHandler` blocklists the download in Sonarr/Radarr and triggers a new search

### Message Queue Architecture (PGMQ)

Three PostgreSQL-backed queues (installed via Flyway migration `V12__install_pgmq.sql`):

| Queue | Message Type | Handler | Concurrency |
|-------|-------------|---------|-------------|
| `nzb_import` | `NzbImportMessage` | `NzbImportService` | 2 workers |
| `nzb_health_check` | `NzbHealthCheckMessage` | `NzbHealthCheckHandler` | 1 worker |
| `nzb_health_repair` | `NzbHealthRepairMessage` | `NzbHealthRepairHandler` | 2 workers |

### Supported Archive Types

nzb-streamer handles: `RAW`, `RAR`, `SEVEN_ZIP`, `RAR_IN_SEVEN_ZIP`, `RAR_IN_RAR`, `SEVEN_ZIP_IN_RAR`, `SEVEN_ZIP_IN_SEVEN_ZIP` (nested archives).

### Database Entities

**`NzbDocumentEntity`** (table `nzb_document`): `files` (JSONB — Yenc headers + segment article IDs), `streamableFiles` (JSONB — file paths with volume/offset positions), `archiveType`, `lastVerified`, `name`, `category`

**`UsenetDownload`**: `status` (QUEUED → DOWNLOADING → COMPLETED/FAILED/ARTICLES_MISSING), `name`, `hash` (MD5 of NZB), `size`, `category`, references `NzbDocumentEntity`

**`NzbImportRecord`** (table `nzb_import`): tracks import queue status with `status`, `archiveType`, `errorMessage`, `files` (JSONB), timestamps

**`NzbContents`** (extends `DebridFileContents`): `originalPath`, `size`, `mimeType`, references `NzbDocumentEntity`

### SABnzbd API Emulation

Endpoints at `/api` (emulating SABnzbd v4.4.0):
- `mode=addfile` — multipart NZB upload
- `mode=queue` — returns queue status
- `mode=history` — completed/failed downloads from DB
- `mode=get_config` — categories and configuration
- `mode=version` — returns "4.4.0"
- `mode=fullstatus` — static status with configured paths

## Configuration

Key properties in `application.properties`:
- `debridav.debrid-clients` - Enabled providers (real-debrid, premiumize, easynews, torbox)
- `debridav.root-path` - WebDAV root path
- `debridav.download-path` - Download directory path
- Provider-specific API keys and settings
11 changes: 0 additions & 11 deletions Dockerfile

This file was deleted.

Loading
Loading