mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-28 09:30:19 +01:00
## Headline features ### Prowlarr plugin - search trackers and download usenet/torrent books - Search any usenet/torrent tracker via Prowlarr, returns books within Universal search - Configure download clients in the app settings (Qbittorrent, Deluge, Transmission, NZBget, SABnzbd) - Unified download and file handling within the app, same as AA. ### IRC plugin - Search IRCHighway #ebooks channel for books and download right in the app. - No setup needed - Credit to OpenBooks for the broad idea and inspiration for best practices for ebook-specific search and download. ### Google Books Metadata Provider - Create a Google Cloud API key and use Google Books as a metadata provider - Not the best source (Hardcover is still recommended), but another option and further redundancy for universal search ### Book series support - New "Series" search field in Hardcover provider - "Series order" sort option - lists books in reading order - "View Series" button in book details modal to search the full series - Series info display (e.g., "3 of 12 in The Wheel of Time") ## Others: - Better format filtering, helpful errors when formats rejected (e.g., "Found 3 ebooks but format not supported (.pdf). Enable in Settings > Formats." - Directory processing - Handles multi-file torrent/usenet downloads properly - Expand search toggle - Skip ISBN search to find more editions - Filtered authors - Uses primary authors only (excludes translators/narrators) for better search results - Language multi-select - Filter releases by multiple languages Docker / Build / Testing - pip cache mounts - Faster Docker builds via BuildKit cache - npm cache mounts - Faster frontend builds - APT cleanup - Smaller final image size - Added make restart command for quick restarts without rebuild - New pytest-based test framework with proper configuration (pyproject.toml) - Unit tests for all download clients (qBittorrent, Transmission, Deluge, NZBGet, SABnzbd) - Bencode parsing tests - Cache tests - Integration tests for Prowlarr handler - E2E test framework
337 lines
11 KiB
Python
337 lines
11 KiB
Python
"""
|
|
E2E Tests for Prowlarr Integration.
|
|
|
|
These tests verify the Prowlarr release source and download client flow.
|
|
Requires Prowlarr and a download client (qBittorrent, Transmission, etc.) to be configured.
|
|
|
|
Run with: docker exec test-cwabd python3 -m pytest tests/e2e/test_prowlarr_flow.py -v -m e2e
|
|
"""
|
|
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from .conftest import APIClient, DownloadTracker
|
|
|
|
|
|
def _is_prowlarr_configured(api_client: APIClient) -> bool:
|
|
"""Check if Prowlarr is configured and available."""
|
|
resp = api_client.get("/api/release-sources")
|
|
if resp.status_code != 200:
|
|
return False
|
|
|
|
sources = resp.json()
|
|
for source in sources:
|
|
if source.get("name") == "prowlarr":
|
|
return True
|
|
return False
|
|
|
|
|
|
def _get_prowlarr_settings(api_client: APIClient) -> dict | None:
|
|
"""Get Prowlarr settings if available."""
|
|
resp = api_client.get("/api/settings/prowlarr")
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return None
|
|
|
|
|
|
def _get_first_provider_name(api_client: APIClient) -> str | None:
|
|
"""Get the first available provider name."""
|
|
providers_resp = api_client.get("/api/metadata/providers")
|
|
if providers_resp.status_code != 200:
|
|
return None
|
|
|
|
providers_data = providers_resp.json()
|
|
if not providers_data:
|
|
return None
|
|
|
|
# Handle both dict and list formats
|
|
if isinstance(providers_data, dict):
|
|
return list(providers_data.keys())[0] if providers_data else None
|
|
else:
|
|
return providers_data[0].get("name") if providers_data else None
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestProwlarrConfiguration:
|
|
"""Tests for Prowlarr configuration."""
|
|
|
|
def test_prowlarr_in_release_sources(self, api_client: APIClient):
|
|
"""Test that Prowlarr appears in release sources."""
|
|
resp = api_client.get("/api/release-sources")
|
|
|
|
assert resp.status_code == 200
|
|
sources = resp.json()
|
|
source_names = [s.get("name") for s in sources]
|
|
assert "prowlarr" in source_names
|
|
|
|
def test_prowlarr_settings_tab_exists(self, api_client: APIClient):
|
|
"""Test that Prowlarr settings tab exists."""
|
|
resp = api_client.get("/api/settings")
|
|
|
|
if resp.status_code == 403:
|
|
pytest.skip("Settings disabled")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
|
|
# Settings may have nested structure with groups/tabs
|
|
if isinstance(data, dict):
|
|
# Could have groups containing tabs, or be flat
|
|
if "groups" in data:
|
|
# Nested: look in groups for prowlarr tabs
|
|
all_tab_names = []
|
|
for group in data.get("groups", []):
|
|
if isinstance(group, dict):
|
|
for tab in group.get("tabs", []):
|
|
if isinstance(tab, dict):
|
|
all_tab_names.append(tab.get("name") or tab.get("id", ""))
|
|
tab_names = all_tab_names
|
|
else:
|
|
tab_names = list(data.keys())
|
|
else:
|
|
tab_names = [t.get("name") or t.get("id") for t in data if isinstance(t, dict)]
|
|
|
|
# Prowlarr settings should exist (may be under different name)
|
|
prowlarr_tabs = [n for n in tab_names if n and "prowlarr" in n.lower()]
|
|
# Also check if we can directly access the prowlarr_clients settings
|
|
prowlarr_resp = api_client.get("/api/settings/prowlarr_clients")
|
|
has_prowlarr_settings = prowlarr_resp.status_code == 200
|
|
|
|
assert prowlarr_tabs or has_prowlarr_settings, f"No prowlarr settings found. Tab names: {tab_names}"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
@pytest.mark.slow
|
|
class TestProwlarrSearch:
|
|
"""Tests for searching via Prowlarr."""
|
|
|
|
def test_prowlarr_search_with_metadata(self, api_client: APIClient):
|
|
"""Test searching Prowlarr with metadata from a provider."""
|
|
if not _is_prowlarr_configured(api_client):
|
|
pytest.skip("Prowlarr not configured")
|
|
|
|
provider = _get_first_provider_name(api_client)
|
|
if not provider:
|
|
pytest.skip("No metadata providers")
|
|
|
|
# Search for a book
|
|
search_resp = api_client.get(
|
|
"/api/metadata/search",
|
|
params={"query": "The Great Gatsby", "provider": provider},
|
|
timeout=30,
|
|
)
|
|
|
|
if search_resp.status_code != 200:
|
|
pytest.skip("Metadata search unavailable")
|
|
|
|
search_data = search_resp.json()
|
|
results = search_data.get("results", search_data)
|
|
|
|
# Handle dict format where results might be nested
|
|
if isinstance(results, dict) and "results" not in results:
|
|
# Results might be the actual result list under a different key
|
|
for key, value in results.items():
|
|
if isinstance(value, list) and value:
|
|
results = value
|
|
break
|
|
|
|
if not results or (isinstance(results, dict) and not results):
|
|
pytest.skip("No metadata results")
|
|
|
|
# Get first result
|
|
if isinstance(results, list):
|
|
book = results[0]
|
|
else:
|
|
pytest.skip("Unexpected results format")
|
|
|
|
book_id = book.get("id") or book.get("provider_id")
|
|
|
|
# Now search releases specifically from Prowlarr
|
|
releases_resp = api_client.get(
|
|
"/api/releases",
|
|
params={
|
|
"provider": provider,
|
|
"book_id": book_id,
|
|
"source": "prowlarr",
|
|
"title": book.get("title", ""),
|
|
"author": book.get("author", ""),
|
|
},
|
|
timeout=60,
|
|
)
|
|
|
|
# Prowlarr may not be reachable
|
|
if releases_resp.status_code == 503:
|
|
pytest.skip("Prowlarr not reachable")
|
|
|
|
if releases_resp.status_code == 200:
|
|
data = releases_resp.json()
|
|
assert "releases" in data
|
|
# Releases may be empty if Prowlarr has no indexers configured
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestProwlarrClientSettings:
|
|
"""Tests for Prowlarr download client settings."""
|
|
|
|
def test_client_settings_structure(self, api_client: APIClient):
|
|
"""Test that client settings have expected structure."""
|
|
resp = api_client.get("/api/settings/prowlarr_clients")
|
|
|
|
if resp.status_code == 403:
|
|
pytest.skip("Settings disabled")
|
|
if resp.status_code == 404:
|
|
pytest.skip("Prowlarr clients settings tab not found")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
|
|
# Should have fields for client configuration
|
|
assert isinstance(data, (dict, list))
|
|
|
|
def test_can_save_client_settings(self, api_client: APIClient):
|
|
"""Test that client settings can be saved."""
|
|
# Get current settings
|
|
get_resp = api_client.get("/api/settings/prowlarr_clients")
|
|
|
|
if get_resp.status_code in [403, 404]:
|
|
pytest.skip("Settings not available")
|
|
|
|
current = get_resp.json()
|
|
|
|
# Try to save the same settings back (no-op save)
|
|
if isinstance(current, dict) and "fields" in current:
|
|
# Extract just the values
|
|
values = {}
|
|
for field in current.get("fields", []):
|
|
key = field.get("key") or field.get("name")
|
|
if key:
|
|
values[key] = field.get("value", "")
|
|
|
|
put_resp = api_client.put(
|
|
"/api/settings/prowlarr_clients",
|
|
json=values,
|
|
)
|
|
# Should succeed (200) or be a no-op
|
|
assert put_resp.status_code in [200, 204, 400]
|
|
|
|
|
|
@pytest.mark.e2e
|
|
@pytest.mark.slow
|
|
class TestProwlarrDownload:
|
|
"""Tests for downloading via Prowlarr."""
|
|
|
|
def test_queue_prowlarr_release(
|
|
self, api_client: APIClient, download_tracker: DownloadTracker
|
|
):
|
|
"""Test queueing a Prowlarr release for download."""
|
|
if not _is_prowlarr_configured(api_client):
|
|
pytest.skip("Prowlarr not configured")
|
|
|
|
provider = _get_first_provider_name(api_client)
|
|
if not provider:
|
|
pytest.skip("No providers")
|
|
|
|
# Search metadata
|
|
search_resp = api_client.get(
|
|
"/api/metadata/search",
|
|
params={"query": "Dracula Bram Stoker", "provider": provider},
|
|
timeout=30,
|
|
)
|
|
|
|
if search_resp.status_code != 200:
|
|
pytest.skip("Metadata search failed")
|
|
|
|
search_data = search_resp.json()
|
|
results = search_data.get("results", search_data)
|
|
|
|
# Handle different result formats
|
|
if isinstance(results, dict):
|
|
for key, value in results.items():
|
|
if isinstance(value, list) and value:
|
|
results = value
|
|
break
|
|
|
|
if not results or not isinstance(results, list):
|
|
pytest.skip("No results")
|
|
|
|
book = results[0]
|
|
book_id = book.get("id") or book.get("provider_id")
|
|
|
|
# Search Prowlarr releases
|
|
releases_resp = api_client.get(
|
|
"/api/releases",
|
|
params={
|
|
"provider": provider,
|
|
"book_id": book_id,
|
|
"source": "prowlarr",
|
|
"title": book.get("title", ""),
|
|
},
|
|
timeout=60,
|
|
)
|
|
|
|
if releases_resp.status_code != 200:
|
|
pytest.skip(f"Releases search failed: {releases_resp.status_code}")
|
|
|
|
releases = releases_resp.json().get("releases", [])
|
|
if not releases:
|
|
pytest.skip("No Prowlarr releases found")
|
|
|
|
# Get the first release
|
|
release = releases[0]
|
|
source_id = release.get("source_id") or release.get("id")
|
|
download_tracker.track(source_id)
|
|
|
|
# Queue it
|
|
queue_resp = api_client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "prowlarr",
|
|
"source_id": source_id,
|
|
"title": release.get("title", book.get("title", "Test")),
|
|
"format": release.get("format"),
|
|
"size": release.get("size"),
|
|
"extra": release.get("extra", {}),
|
|
},
|
|
)
|
|
|
|
# May fail if no download client configured
|
|
if queue_resp.status_code == 200:
|
|
data = queue_resp.json()
|
|
assert data.get("status") == "queued"
|
|
|
|
# Wait briefly and check status
|
|
time.sleep(3)
|
|
|
|
status_resp = api_client.get("/api/status")
|
|
if status_resp.status_code == 200:
|
|
status_data = status_resp.json()
|
|
# Should be in one of the status categories
|
|
found = False
|
|
for category in status_data.values():
|
|
if isinstance(category, dict) and source_id in category:
|
|
found = True
|
|
break
|
|
# It's ok if not found (may have already processed/errored)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestProwlarrClientConnection:
|
|
"""Tests for testing download client connections."""
|
|
|
|
def test_connection_test_action(self, api_client: APIClient):
|
|
"""Test the connection test action for download clients."""
|
|
# This tests the action button functionality in settings
|
|
resp = api_client.post(
|
|
"/api/settings/prowlarr_clients/action/test_torrent_connection"
|
|
)
|
|
|
|
# May succeed, fail, or not exist depending on configuration
|
|
# We just verify it returns a response
|
|
assert resp.status_code in [200, 400, 404, 500]
|
|
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
# Should have success/message structure
|
|
assert "success" in data or "message" in data or "result" in data
|