Compare commits

..
Author SHA1 Message Date
cabbage d951e2e4fc fix: require qBittorrent metainfo export support 2026-07-24 12:37:40 +00:00
cabbage 21ea431c56 fix: expand configured service usernames 2026-07-23 16:34:37 +00:00
12 changed files with 27 additions and 16 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: archive-control-archive
services:
archive-client:
image: sodium/archive-clients:v0.1.0
image: sodium/archive-clients:v0.1.2
user: "1000:1000"
restart: unless-stopped
command: ["--config", "/etc/archive-control/client.toml"]
+1 -1
View File
@@ -2,7 +2,7 @@ name: archive-control-cache
services:
archive-client:
image: sodium/archive-clients:v0.1.0
image: sodium/archive-clients:v0.1.2
user: "1001:1001"
restart: unless-stopped
network_mode: host
+1 -1
View File
@@ -2,7 +2,7 @@ name: archive-control-cache
services:
archive-client:
image: sodium/archive-clients:v0.1.0
image: sodium/archive-clients:v0.1.2
user: "1001:1001"
restart: unless-stopped
network_mode: host
+1 -1
View File
@@ -213,7 +213,7 @@ cache/archive routes according to policy.
```yaml
services:
archive-client:
image: sodium/archive-clients:v0.1.0
image: sodium/archive-clients:v0.1.2
user: "1001:1001"
restart: unless-stopped
command: ["archive-client", "--config", "/etc/archive-control/client.toml"]
+2 -1
View File
@@ -105,7 +105,8 @@ follow-up task.
### Actions
1. Implement qBittorrent cookie authentication and capability adapters for
supported 4.44.6 and 5.x APIs.
supported 4.54.6 and 5.x APIs; reject older releases because they cannot
export exact torrent metainfo through the public API.
2. Implement on-demand torrent summaries, scoped hash lookup, lazy complete
content trees, v1/v2 identity validation, torrent export, stopped add,
selection application, recheck monitoring, and entry-only deletion.
+2 -2
View File
@@ -6,7 +6,7 @@ shapes.
## qBittorrent Web API
The implementation targets supported qBittorrent 4.44.6 and 5.x releases and
The implementation targets qBittorrent 4.54.6 and 5.x releases and
detects the application and Web API versions at startup. The authoritative
references are the official [5.0 WebUI API](https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-%28qBittorrent-5.0%29)
and [4.1-compatible WebUI API](https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-%28qBittorrent-4.1%29).
@@ -16,7 +16,7 @@ and [4.1-compatible WebUI API](https://github.com/qbittorrent/qBittorrent/wiki/W
| Need | Web API family | Archive Control rule |
| --- | --- | --- |
| Authenticate | `auth/login`, cookie session | Log no credentials/cookies; reauthenticate once on expiry |
| Detect compatibility | `app/version`, `app/webapiVersion`, build info | Advertise exact versions and select adapter |
| Detect compatibility | `app/version`, `app/webapiVersion`, build info | Require qBittorrent 4.5.0+ because exact metainfo export is mandatory |
| List/lookup | `torrents/info`, properties | On-demand and hash-scoped where possible |
| Read file state | `torrents/files` | Normalize indices, paths, selected/skipped, size, progress |
| Export metainfo | `torrents/export` | Required before staging; fail if exact metainfo unavailable |
+2 -2
View File
@@ -89,8 +89,8 @@ overlapping selections, reordered/duplicated events, and partial journals.
### Adapter contract tests
Pinned qBittorrent and Syncthing container versions cover every supported API
family. Assertions include stopped add, exact selection, export, full recheck,
Pinned qBittorrent 4.54.6 and 5.x and Syncthing container versions cover every
supported API family. Assertions include stopped add, exact selection, export, full recheck,
download-attempt detection, entry-only delete, folder/device idempotency,
events fallback, need/completion proof, and redacted errors. Partfile adapters
are tested only against explicitly supported qBittorrent/libtorrent fixtures.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "archive-clients"
version = "0.1.0"
version = "0.1.2"
requires-python = ">=3.11"
dependencies = ["protobuf==7.35.1", "websockets==16.0"]
+2
View File
@@ -169,6 +169,8 @@ def _service(value: Any, name: str) -> ServiceConfig:
username = value.get("username")
if username is not None and (not isinstance(username, str) or not username):
raise ConfigError(f"{name}.username must be a non-empty string")
if username is not None:
username = _expand(username)
addresses = value.get("advertised_addresses", [])
if not isinstance(addresses, list) or any(
not isinstance(address, str) or not address for address in addresses
+3 -1
View File
@@ -159,7 +159,9 @@ def _supported_qb_version(version: str) -> bool:
if match is None:
return False
major, minor = int(match.group(1)), int(match.group(2))
return major == 5 or (major == 4 and minor in {4, 5, 6})
# torrents/export, required to preserve exact metainfo before staging,
# was introduced in qBittorrent 4.5.0.
return major == 5 or (major == 4 and minor in {5, 6})
def _text_get(
+8 -2
View File
@@ -2,6 +2,7 @@ import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from archive_clients.config import ClientConfig, ConfigError, RootMapping
@@ -18,10 +19,15 @@ class ConfigTests(unittest.TestCase):
(root / "sync").mkdir()
config_path = root / "client.toml"
config_path.write_text(
_config(root, role="cache"), encoding="utf-8"
_config(root, role="cache").replace(
'username = "admin"', 'username = "${QB_USER}"'
),
encoding="utf-8",
)
config = ClientConfig.load(config_path, "archive")
with patch.dict(os.environ, {"QB_USER": "admin"}):
config = ClientConfig.load(config_path, "archive")
self.assertEqual(config.role, "archive")
self.assertEqual(config.qbittorrent.username, "admin")
self.assertEqual(config.read_shared_token(), "token")
self.assertEqual(
config.qbittorrent.roots.api_to_local("/downloads/a/b"),
+3 -3
View File
@@ -43,11 +43,11 @@ class _Opener:
class ServiceProbeTests(unittest.TestCase):
def test_supported_qbittorrent_versions_include_live_4_4_api(self):
for version in ("v4.4.5", "v4.5.5", "v4.6.7", "v5.2.3"):
def test_supported_qbittorrent_versions_require_export_capability(self):
for version in ("v4.5.0", "v4.6.7", "v5.2.3"):
with self.subTest(version=version):
self.assertTrue(_supported_qb_version(version))
for version in ("v4.3.9", "v6.0.0", "invalid"):
for version in ("v4.3.9", "v4.4.5", "v6.0.0", "invalid"):
with self.subTest(version=version):
self.assertFalse(_supported_qb_version(version))