Files
shelfmark/tests/audiobookbay/test_utils.py
T
bonsai-dreams dd6fd1e199 Feature: Add AudiobookBay release source (#619)
## Summary

Adds AudiobookBay as a web-scraping release source for audiobook
torrents. Once enabled, a new tab shows up in the Find Releases modal.

## What's New

- **AudiobookBay source** – Search AudiobookBay for audiobook torrents
from the Shelfmark UI
- **Torrent downloads** – Extract magnet links from detail pages and add
them to the configured torrent client
- **Audiobook-only** – Source is limited to audiobooks
- **Download clients:** Currently uses the torrent client configured
under **Prowlarr > Download Clients**.
- Audiobook-specific categories (e.g. `QBITTORRENT_CATEGORY_AUDIOBOOK`)
are applied when set.
- **Settings → AudiobookBay**:
  - Enable toggle
  - Hostname
  - Max pages to search (default 5)
  - Rate limit delay in seconds (default 1)


## How It Works

1. User searches for an audiobook; AudiobookBay is queried if enabled.
2. Results show title, language, format, and size
3. User selects a release; the handler fetches the detail page and
extracts the magnet link.
4. Magnet link is sent to the configured torrent client.

## Testing

- Unit tests for source, handler, scraper, and utils
- Mocked HTTP requests and torrent client calls
- Coverage for search, relevance filtering, language mapping, size
parsing, and download flow

### Screenshots
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/2e10a259-5c35-4065-980d-b59a1c961c9f"
/>
2026-02-16 14:44:47 +00:00

104 lines
4.1 KiB
Python

"""
Tests for AudiobookBay utility functions.
"""
import pytest
from shelfmark.release_sources.audiobookbay.utils import parse_size, sanitize_title
class TestParseSize:
"""Tests for the parse_size function."""
def test_parse_size_bytes(self):
"""Test parsing byte sizes."""
assert parse_size("100 B") == 100
assert parse_size("512 B") == 512
assert parse_size("100B") == 100 # No space
def test_parse_size_kilobytes(self):
"""Test parsing kilobyte sizes."""
assert parse_size("1 KB") == 1024
assert parse_size("2 KB") == 2048
assert parse_size("1.5 KB") == int(1.5 * 1024)
assert parse_size("1KBs") == 1024 # Handles "KBs" suffix
def test_parse_size_megabytes(self):
"""Test parsing megabyte sizes."""
assert parse_size("1 MB") == 1024 ** 2
assert parse_size("500 MB") == 500 * (1024 ** 2)
assert parse_size("1.5 MB") == int(1.5 * (1024 ** 2))
assert parse_size("500.00 MBs") == int(500.00 * (1024 ** 2)) # Handles "MBs" suffix
def test_parse_size_gigabytes(self):
"""Test parsing gigabyte sizes."""
assert parse_size("1 GB") == 1024 ** 3
assert parse_size("11.68 GB") == int(11.68 * (1024 ** 3))
assert parse_size("1.01 GBs") == int(1.01 * (1024 ** 3)) # Handles "GBs" suffix
def test_parse_size_terabytes(self):
"""Test parsing terabyte sizes."""
assert parse_size("1 TB") == 1024 ** 4
assert parse_size("2.5 TB") == int(2.5 * (1024 ** 4))
def test_parse_size_case_insensitive(self):
"""Test that size parsing is case insensitive."""
assert parse_size("1 gb") == 1024 ** 3
assert parse_size("1 Gb") == 1024 ** 3
assert parse_size("1 GB") == 1024 ** 3
assert parse_size("1 gbs") == 1024 ** 3
def test_parse_size_none(self):
"""Test that None returns None."""
assert parse_size(None) is None
def test_parse_size_empty_string(self):
"""Test that empty string returns None."""
assert parse_size("") is None
def test_parse_size_invalid_format(self):
"""Test that invalid formats return None."""
assert parse_size("invalid") is None
assert parse_size("123") is None # No unit
assert parse_size("abc MB") is None # Invalid number
def test_parse_size_with_whitespace(self):
"""Test parsing with various whitespace."""
assert parse_size(" 1 GB ") == 1024 ** 3
assert parse_size("1.5\tMB") == int(1.5 * (1024 ** 2))
class TestSanitizeTitle:
"""Tests for the sanitize_title function."""
def test_sanitize_title_removes_invalid_chars(self):
"""Test removing invalid filename characters."""
assert sanitize_title("Test<Book>") == "TestBook"
assert sanitize_title("Test:Book") == "TestBook"
assert sanitize_title("Test/Book") == "TestBook"
assert sanitize_title("Test\\Book") == "TestBook"
assert sanitize_title("Test|Book") == "TestBook"
assert sanitize_title("Test?Book") == "TestBook"
assert sanitize_title("Test*Book") == "TestBook"
assert sanitize_title('Test"Book') == "TestBook"
def test_sanitize_title_preserves_valid_chars(self):
"""Test that valid characters are preserved."""
assert sanitize_title("Test Book - Author") == "Test Book - Author"
assert sanitize_title("Test Book (2024)") == "Test Book (2024)"
assert sanitize_title("Test Book [Special]") == "Test Book [Special]"
assert sanitize_title("Test Book's Title") == "Test Book's Title"
def test_sanitize_title_strips_whitespace(self):
"""Test that leading/trailing whitespace is stripped."""
assert sanitize_title(" Test Book ") == "Test Book"
assert sanitize_title("\tTest Book\n") == "Test Book"
def test_sanitize_title_empty(self):
"""Test that empty string returns empty."""
assert sanitize_title("") == ""
def test_sanitize_title_multiple_invalid_chars(self):
"""Test removing multiple invalid characters."""
assert sanitize_title("Test<Book>:Author/Title") == "TestBookAuthorTitle"