Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ede990a
Remove stale Docker test infrastructure
nitrobass24 May 1, 2026
2b516b2
Rewrite test-image Dockerfile as Alpine with Python 3.13
nitrobass24 May 1, 2026
1007581
Merge pull request #434 from nitrobass24/chore/remove-stale-test-dock…
nitrobass24 May 1, 2026
c0e4332
Collapse Dockerfile to 2 stages with Python 3.13-alpine
nitrobass24 May 1, 2026
9903e45
Merge pull request #435 from nitrobass24/chore/test-image-alpine
nitrobass24 May 1, 2026
51ede44
Merge pull request #436 from nitrobass24/chore/dockerfile-python313-a…
nitrobass24 May 1, 2026
cf856e6
Use RUN --mount for uv to cut image size from 114 MB to 64 MB (#437)
nitrobass24 May 1, 2026
5272389
Flush configs on write, differentiated restart notifications, LFTP ho…
nitrobass24 May 1, 2026
750f94d
PR 9: E2E — File Actions & Error States (#438)
nitrobass24 May 1, 2026
eff4a4b
Add Python integration tests to CI (#449)
nitrobass24 May 1, 2026
9e95c56
Add security middleware unit tests (47 tests) (#439)
nitrobass24 May 1, 2026
ed2bdfe
Add controller core unit tests (36 tests) (#444)
nitrobass24 May 1, 2026
8d93a20
PR 3: Python — Web App Job & Context tests (#446)
nitrobass24 May 1, 2026
5a2578a
PR 4: Python — Handler Integration Test Expansion (#447)
nitrobass24 May 2, 2026
8f3d45e
Add ViewFileFilterService tests (18 tests) (#440)
nitrobass24 May 2, 2026
c01650b
Add E2E tests for integrations CRUD (#441)
nitrobass24 May 2, 2026
1d48e9e
Add AutoQueueService and PathPairsService tests (25 tests) (#445)
nitrobass24 May 2, 2026
2866c04
Add FileOptions, Integrations, and Option component tests (28 tests) …
nitrobass24 May 2, 2026
55d77c6
Add HeaderComponent and VersionCheckService tests (20 tests) (#443)
nitrobass24 May 2, 2026
c3f8a91
Cache Playwright browsers and npm deps in CI (#452)
nitrobass24 May 2, 2026
12047c5
Track Playwright package-lock.json so CI cache step resolves (#453)
nitrobass24 May 2, 2026
cbe75f3
Add test count badges and update Python version to 3.13 (#451)
nitrobass24 May 2, 2026
0cc3533
PR 11: E2E — Settings Coverage Expansion (#442)
nitrobass24 May 2, 2026
dc6b73b
Refresh README to cover features through v0.17.0 (#454)
nitrobass24 May 2, 2026
be50a36
Fix flaky Hash Algorithm select wait condition (#455)
nitrobass24 May 2, 2026
142dc62
Warm Playwright cache on develop and bump workers to 10 (#456)
nitrobass24 May 2, 2026
3b66451
Stop leaking StreamHandlers across test runs (#450) (#457)
nitrobass24 May 3, 2026
30d2977
chore(deps): bump @docusaurus/preset-classic in /website (#458)
dependabot[bot] May 4, 2026
cbe98eb
chore(deps-dev): bump @docusaurus/faster in /website (#460)
dependabot[bot] May 4, 2026
806ed99
chore(deps): bump the angular group in /src/angular with 10 updates (…
dependabot[bot] May 4, 2026
fc6c9bf
chore(deps-dev): bump typescript-eslint in /src/angular (#464)
dependabot[bot] May 4, 2026
3a487fe
chore(deps-dev): bump jsdom from 29.0.2 to 29.1.1 in /src/angular (#465)
dependabot[bot] May 4, 2026
a301510
chore(deps-dev): bump @docusaurus/module-type-aliases in /website (#459)
dependabot[bot] May 4, 2026
899fc1e
chore(deps-dev): bump eslint from 10.2.1 to 10.3.0 in /src/angular (#…
dependabot[bot] May 4, 2026
9376c54
chore(deps-dev): bump @docusaurus/types in /website (#461)
dependabot[bot] May 4, 2026
660f99b
chore(deps): bump @docusaurus/core from 3.10.0 to 3.10.1 in /website …
dependabot[bot] May 4, 2026
ecfa8f5
Address CodeRabbit test-quality findings (#468)
nitrobass24 May 5, 2026
04d6320
Merge branch 'master' into develop
nitrobass24 May 5, 2026
a958f6a
Address round-2 CodeRabbit test-quality findings (#470)
nitrobass24 May 5, 2026
65c1339
Address round-3 CodeRabbit findings (#471)
nitrobass24 May 5, 2026
7cc5de4
Pin Python builder to alpine3.23 to match the runtime stage (#472)
nitrobass24 May 5, 2026
a317805
Address round-4 CodeRabbit findings on PR #467
nitrobass24 May 5, 2026
e0f7ee6
Address round-5 CodeRabbit findings on PR #467
nitrobass24 May 5, 2026
1c3241c
Address round-6 CodeRabbit findings on PR #467
nitrobass24 May 6, 2026
5bc7aa5
Address round-7 CodeRabbit findings on PR #467
nitrobass24 May 6, 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
62 changes: 44 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
python-version: "3.13"

- name: Set up uv
uses: astral-sh/setup-uv@v7
Expand All @@ -101,7 +101,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
python-version: "3.13"

- name: Set up uv
uses: astral-sh/setup-uv@v7
Expand All @@ -125,7 +125,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
python-version: "3.13"

- name: Set up uv
uses: astral-sh/setup-uv@v7
Expand All @@ -148,6 +148,34 @@ jobs:
--ignore=tests/unittests/test_controller/test_scan/test_remote_scanner.py
--ignore=tests/unittests/test_common/test_multiprocessing_logger.py

python-integration-test:
name: Python Integration Tests
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Set up uv
uses: astral-sh/setup-uv@v7

- name: Install dependencies
working-directory: src/python
run: uv pip install --system -r pyproject.toml --group test

- name: Run web handler integration tests
working-directory: src/python
env:
PYTHONPATH: .
run: >-
pytest tests/integration/test_web -v --tb=short
--timeout=30

angular-build:
name: Build Angular
if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/develop' && github.event_name == 'push') || github.event_name == 'workflow_dispatch'
Expand Down Expand Up @@ -180,7 +208,7 @@ jobs:

build-test:
name: Build and Test (amd64)
if: ${{ !(startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/develop' && github.event_name == 'push') || github.event_name == 'workflow_dispatch') }}
if: ${{ !(startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down Expand Up @@ -211,20 +239,17 @@ jobs:
done
docker logs test-container

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22

- name: Install Playwright
working-directory: src/e2e-playwright
- name: Run E2E tests in Playwright container
run: |
npm install
npx playwright install --with-deps chromium

- name: Run E2E tests
working-directory: src/e2e-playwright
run: npx playwright test
# Match the @playwright/test version pinned in the lockfile so the
# container's chromium build is the one the package expects.
PW_VERSION=$(jq -r '.packages["node_modules/@playwright/test"].version' src/e2e-playwright/package-lock.json)
docker run --rm \
--network host \
-v "$GITHUB_WORKSPACE":/workspace \
-w /workspace/src/e2e-playwright \
"mcr.microsoft.com/playwright:v${PW_VERSION}-noble" \
sh -c "npm ci && npx playwright test"

- name: Upload Playwright report
if: failure()
Expand Down Expand Up @@ -392,9 +417,10 @@ jobs:
&& needs.python-lint.result == 'success'
&& needs.python-typecheck.result == 'success'
&& needs.python-test.result == 'success'
&& needs.python-integration-test.result == 'success'
&& needs.build-amd64.result == 'success'
&& needs.build-arm64.result == 'success'
needs: [unit-test, angular-lint, python-lint, python-typecheck, python-test, build-amd64, build-arm64]
needs: [unit-test, angular-lint, python-lint, python-typecheck, python-test, python-integration-test, build-amd64, build-arm64]
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package-lock.json
!src/angular/package-lock.json
!website/package-lock.json
!src/e2e-playwright/package-lock.json
src/python/build
src/python/site
dev-config/
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# Changelog

## [0.18.0] - 2026-05-04

### Changed

- **Image size reduced from 114 MB to 64 MB** — `RUN --mount=from=ghcr.io/astral-sh/uv` keeps `uv` out of the runtime image; build artifacts no longer persist (#437)
- **Dockerfile collapsed to 2 stages on Python 3.13-alpine** — Removes an intermediate stage and the legacy Buster build path (#436)
- **Test image rewritten as Alpine with Python 3.13** (#435)
- **Removed stale Docker test infrastructure** — Old Compose files and fixture data carried over from the Protractor era (#434)
- **Playwright E2E migrated to the official `mcr.microsoft.com/playwright` container** — Eliminates host-side browser install and `apt install` of system deps; image tag auto-pinned to the `@playwright/test` version from `package-lock.json` (#456)
- **Build and Test (amd64) runs on develop pushes** — Populates the GHA cache so PRs against develop hit a warm cache instead of paying the install cost on every run (#456)
- **Playwright browser cache + npm cache wired through `actions/cache`** (#452, #453)
- **Python 3.12 → 3.13** in CI and Dockerfile (#451)
- **README refreshed** to cover features through v0.17.0 — Notifications, Sonarr/Radarr, integrity verification, staging, API key (#454)
- **Test count badges** added to the README (#451)
- **Dependency updates** — Angular group (10 packages), typescript-eslint, jsdom, eslint, Docusaurus 3.10.0 → 3.10.1 across 5 packages (#458, #459, #460, #461, #463, #464, #465, #466)

### Added

- **LFTP hot-reload** — Connection-related settings (parallel connections, max total connections, socket buffer size, bandwidth limit) apply without a container restart. Settings UI distinguishes between options that take effect immediately and those that require restart (#433)
- **Atomic config writes** — `Config.to_file()` now flushes to disk via temp file + `os.rename` so a process kill mid-write can't truncate the config (#433)
- **Expanded unit and E2E test coverage:**
- 47 security middleware unit tests (#439)
- 36 controller core unit tests (#444)
- 28 FileOptions / Integrations / Option component tests (#448)
- 25 AutoQueueService and PathPairsService tests (#445)
- 20 HeaderComponent and VersionCheckService tests (#443)
- 18 ViewFileFilterService tests (#440)
- E2E for integrations CRUD (#441)
- E2E for File Actions & Error States (#438)
- E2E Settings coverage expansion (#442)
- Web App Job & Context Python tests (#446)
- Handler integration test expansion (#447)
- Python integration tests added to CI (#449)

### Fixed

- **Hash Algorithm select test flake** — Test waited on the wrong config field; the algorithm select's disable gate is `validate.enabled`, not `validate.xfer_verify`. Test now sets the correct field and waits for the SSE-delivered model value before asserting (#455)
- **StreamHandler leaks in test setUp methods** — Tests added a handler to the shared root logger but never removed it. Loggers are singletons, so by test N the logger had N handlers and each log line printed N times. Fixed via `self.addCleanup(logger.removeHandler, handler)` across 8 test files / 10 setUp methods (#450, #457)
- **Multiprocessing resource leak in `test_active_scanner.py`** — `scanner.close()` now registered with `addCleanup` so resources release even if assertions raise

### Security

- **`apk` and its package database are stripped from the runtime image** — `/sbin/apk`, `/etc/apk`, `/var/cache/apk`, `/lib/apk`, and the `libapk` shared libraries are removed in the runtime stage to keep the image under 64 MB (#437). This means in-image vulnerability scanners (Trivy, Grype) can't enumerate Alpine packages directly from a running container; consumers should scan the published `ghcr.io/nitrobass24/seedsync` image via SBOM or image-history tooling. Affected runtime packages: `lftp`, `openssh-client`, `ca-certificates`, `setpriv`, `libstdc++`. Derived images that need `apk add` should base on Alpine and reinstall what they need rather than extending this image.

## [0.17.0] - 2026-04-30

### Added
Expand Down
74 changes: 55 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
<img src="https://img.shields.io/badge/docs-website-blue" alt="Documentation">
</a>
<img src="https://img.shields.io/badge/Angular-21-dd0031" alt="Angular 21">
<img src="https://img.shields.io/badge/Python-3.12-3776ab" alt="Python 3.12">
<img src="https://img.shields.io/badge/Python-3.13-3776ab" alt="Python 3.13">
<img src="https://img.shields.io/badge/platform-amd64%20%7C%20arm64-lightgrey" alt="Platform">
<br>
<img src="https://img.shields.io/badge/Python_tests-828-2ea44f" alt="Python Tests: 828">
<img src="https://img.shields.io/badge/Angular_tests-412-2ea44f" alt="Angular Tests: 412">
<img src="https://img.shields.io/badge/E2E_tests-95-2ea44f" alt="E2E Tests: 95">
</p>

SeedSync is a tool to sync files from a remote Linux server (like your seedbox) to your local machine.
Expand All @@ -36,16 +40,19 @@ It uses LFTP to transfer files fast!

## Features

* Built on top of [LFTP](http://lftp.tech/), the fastest file transfer program
* Web UI — track and control your transfers from anywhere
* **Multiple path pairs** — sync from multiple remote directories independently
* **Exclude patterns** — filter out unwanted files with glob patterns
* **Multi-select** — select multiple files for bulk queue/stop/delete
* Auto-Queue — only sync the files you want based on pattern matching
* Automatically extract your files after sync
* **Webhook notifications** — HTTP POST on download/extract events
* Delete local and remote files easily
* Dark mode, staging directory, bandwidth limiting, and more
* Built on top of [LFTP](http://lftp.tech/) — the fastest file transfer tool around
* Web UI — track and control transfers from any browser
* **Multiple path pairs** — sync from independent remote/local directory pairs, each with its own settings
* **Auto-Queue** — automatically queue files matching your patterns
* **Exclude patterns** — filter out unwanted files (`*.nfo`, `Sample/`, etc.) per path pair
* **Multi-select bulk actions** — queue, stop, or delete multiple files at once
* **Auto-extract** — unpack RAR, ZIP, 7z, tar, gz, bz2, xz archives after download
* **File integrity verification** — inline checksum during download plus optional post-download validation
* **Notifications** — Discord, Telegram, or generic webhook on download/extract events
* **Sonarr/Radarr integration** — trigger imports with multiple named instances, attached per path pair
* **Staging directory** — land downloads on fast storage, then move to the final location
* **Optional API key auth** with CSRF protection and rate limiting
* **Dark mode**, bandwidth limiting, virtual scrolling for large libraries, and more
* **Lightweight Docker image** — ~45 MB Alpine-based, multi-arch (amd64/arm64)
* Fully open source!

Expand Down Expand Up @@ -121,6 +128,20 @@ SeedSync is available as a Community Application on Unraid.

> **Note**: PUID/PGID default to `99`/`100` (Unraid's `nobody`/`users`), which is correct for most Unraid setups.

## Recommended Workflow

The best way to use SeedSync is with **hard links** and a dedicated completion directory:

1. **Configure your torrent client** (qBittorrent, ruTorrent, etc.) to hard link completed downloads into a separate folder (e.g., `/downloads/complete`). Hard links don't use extra disk space — your originals stay intact for seeding.
2. **Point SeedSync** at the completion directory.
3. **Enable Auto-Queue** and turn on **"Delete remote file after syncing"** in Settings.

This way, each file is downloaded exactly once. After SeedSync syncs it, the hard link is removed from the completion directory, so it's never re-downloaded — even after a container restart. Your originals remain untouched for seeding.

> **Note**: Both directories must be on the same filesystem for hard links to work.

See the [full setup guide](https://nitrobass24.github.io/seedsync/usage#recommended-setup) in the docs for directory layout examples and torrent client configuration.

## Configuration

On first run, access the web UI and configure:
Expand Down Expand Up @@ -149,19 +170,33 @@ You can limit download speed in Settings under the **Connections** section. The
- Values with suffixes: `K` for KB/s, `M` for MB/s (e.g., `500K`, `2M`)
- `0` or empty for unlimited

## Recommended Workflow
### Notifications

The best way to use SeedSync is with **hard links** and a dedicated completion directory:
SeedSync can notify you when downloads or extractions finish. Configure destinations in **Settings → Notifications**:

1. **Configure your torrent client** (qBittorrent, ruTorrent, etc.) to hard link completed downloads into a separate folder (e.g., `/downloads/complete`). Hard links don't use extra disk space — your originals stay intact for seeding.
2. **Point SeedSync** at the completion directory.
3. **Enable Auto-Queue** and turn on **"Delete remote file after syncing"** in Settings.
- **Discord** — paste a webhook URL; events arrive as color-coded embeds
- **Telegram** — provide a bot token and chat ID
- **Generic webhook** — POST a JSON payload to any HTTP(S) URL

This way, each file is downloaded exactly once. After SeedSync syncs it, the hard link is removed from the completion directory, so it's never re-downloaded — even after a container restart. Your originals remain untouched for seeding.
Each destination has a **Test** button to verify it's working.

> **Note**: Both directories must be on the same filesystem for hard links to work.
### Sonarr / Radarr

See the [full setup guide](https://nitrobass24.github.io/seedsync/usage#recommended-setup) in the docs for directory layout examples and torrent client configuration.
To trigger automatic imports after downloads complete, configure **Settings → Integrations**:

1. Click **Add instance**, choose Sonarr or Radarr, and enter the base URL and API key
2. Use **Test connection** to verify credentials
3. In the **Path Pairs** card, attach instances to the path pair(s) where imports should fire

You can mix and match — for example, attach a 4K path pair only to your 4K Radarr instance.

### Staging Directory

For setups with fast scratch storage (NVMe/SSD) and slower bulk storage, enable **Staging** in Settings. Downloads complete on the staging volume first, then move to the final location. Mount your staging path at `/staging` in the container.

### API Key

To require authentication on the web UI and API, set an **API Key** in **Settings → Other Settings**. When set, browser clients prompt for the key on first load and API clients send it via the `X-API-Key` header. Leave it blank to run without auth (fine behind a reverse proxy or on a trusted network).

## Building from Source

Expand Down Expand Up @@ -192,6 +227,7 @@ make logs
|------|-------------|
| `/config` | Configuration and state files |
| `/downloads` | Download destination directory |
| `/staging` | Staging directory (optional, mount when staging is enabled) |
| `/home/seedsync/.ssh/id_rsa` | SSH private key (optional, for key-based auth) |

## Ports
Expand Down
Loading
Loading