mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-03 20:05:46 +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
122 lines
4.2 KiB
Python
122 lines
4.2 KiB
Python
"""
|
|
Tests for the Prowlarr release cache.
|
|
"""
|
|
|
|
import time
|
|
import pytest
|
|
|
|
# Import the cache module
|
|
from cwa_book_downloader.release_sources.prowlarr import cache
|
|
|
|
|
|
class TestProwlarrCache:
|
|
"""Tests for release caching functionality."""
|
|
|
|
def setup_method(self):
|
|
"""Clear cache before each test."""
|
|
# Clear the internal cache
|
|
cache._cache.clear()
|
|
|
|
def test_cache_release_stores_data(self):
|
|
"""Test that cache_release stores data correctly."""
|
|
release_data = {"title": "Test Book", "size": 1024}
|
|
cache.cache_release("test-id", release_data)
|
|
|
|
assert "test-id" in cache._cache
|
|
stored_data, timestamp = cache._cache["test-id"]
|
|
assert stored_data == release_data
|
|
assert isinstance(timestamp, float)
|
|
|
|
def test_get_release_returns_cached_data(self):
|
|
"""Test that get_release returns cached data."""
|
|
release_data = {"title": "Test Book", "format": "epub"}
|
|
cache.cache_release("get-test-id", release_data)
|
|
|
|
result = cache.get_release("get-test-id")
|
|
assert result == release_data
|
|
|
|
def test_get_release_returns_none_for_missing_id(self):
|
|
"""Test that get_release returns None for non-existent IDs."""
|
|
result = cache.get_release("non-existent-id")
|
|
assert result is None
|
|
|
|
def test_get_release_returns_none_for_expired(self, monkeypatch):
|
|
"""Test that get_release returns None for expired entries."""
|
|
release_data = {"title": "Old Book"}
|
|
cache.cache_release("expired-id", release_data)
|
|
|
|
# Make the entry appear old by modifying the TTL check
|
|
original_ttl = cache.RELEASE_CACHE_TTL
|
|
monkeypatch.setattr(cache, "RELEASE_CACHE_TTL", 0)
|
|
|
|
# Wait a tiny bit to ensure expiration
|
|
time.sleep(0.01)
|
|
|
|
result = cache.get_release("expired-id")
|
|
assert result is None
|
|
|
|
# Restore TTL
|
|
monkeypatch.setattr(cache, "RELEASE_CACHE_TTL", original_ttl)
|
|
|
|
def test_remove_release_deletes_entry(self):
|
|
"""Test that remove_release removes cached entries."""
|
|
cache.cache_release("remove-id", {"title": "Book to Remove"})
|
|
assert "remove-id" in cache._cache
|
|
|
|
cache.remove_release("remove-id")
|
|
assert "remove-id" not in cache._cache
|
|
|
|
def test_remove_release_ignores_missing_ids(self):
|
|
"""Test that remove_release doesn't raise for missing IDs."""
|
|
# Should not raise any exception
|
|
cache.remove_release("never-existed")
|
|
|
|
def test_cleanup_expired_removes_old_entries(self, monkeypatch):
|
|
"""Test that cleanup_expired removes old entries."""
|
|
# Add some entries
|
|
cache.cache_release("keep-id", {"title": "Keep This"})
|
|
cache.cache_release("old-id", {"title": "Remove This"})
|
|
|
|
# Make old-id appear expired by manipulating its timestamp
|
|
cache._cache["old-id"] = ({"title": "Remove This"}, time.time() - 7200) # 2 hours old
|
|
|
|
removed = cache.cleanup_expired()
|
|
|
|
assert removed == 1
|
|
assert "keep-id" in cache._cache
|
|
assert "old-id" not in cache._cache
|
|
|
|
def test_get_cache_stats_returns_correct_info(self):
|
|
"""Test that get_cache_stats returns accurate information."""
|
|
cache.cache_release("stat-1", {"title": "Book 1"})
|
|
cache.cache_release("stat-2", {"title": "Book 2"})
|
|
|
|
stats = cache.get_cache_stats()
|
|
|
|
assert stats["size"] == 2
|
|
assert "stat-1" in stats["entries"]
|
|
assert "stat-2" in stats["entries"]
|
|
|
|
def test_cache_is_thread_safe(self):
|
|
"""Test that cache operations are thread-safe."""
|
|
import threading
|
|
|
|
errors = []
|
|
|
|
def cache_operations():
|
|
try:
|
|
for i in range(100):
|
|
cache.cache_release(f"thread-{threading.current_thread().name}-{i}", {"data": i})
|
|
cache.get_release(f"thread-{threading.current_thread().name}-{i}")
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
threads = [threading.Thread(target=cache_operations, name=f"T{i}") for i in range(5)]
|
|
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
assert len(errors) == 0, f"Thread errors: {errors}"
|