diff --git a/shelfmark/core/models.py b/shelfmark/core/models.py index ce7befec..a5286db3 100644 --- a/shelfmark/core/models.py +++ b/shelfmark/core/models.py @@ -76,6 +76,7 @@ class DownloadTask: size: Optional[str] = None preview: Optional[str] = None content_type: Optional[str] = None # "book (fiction)", "audiobook", "magazine", etc. + source_url: Optional[str] = None # Original release URL used by source-specific handlers # Series info (for library naming templates) series_name: Optional[str] = None diff --git a/shelfmark/download/orchestrator.py b/shelfmark/download/orchestrator.py index 81f45bcd..30aa4b5b 100644 --- a/shelfmark/download/orchestrator.py +++ b/shelfmark/download/orchestrator.py @@ -188,6 +188,16 @@ def queue_release( year = release_data.get('year') or extra.get('year') preview = release_data.get('preview') or extra.get('preview') content_type = release_data.get('content_type') or extra.get('content_type') + source_url_raw = ( + release_data.get('download_url') + or release_data.get('source_url') + or release_data.get('info_url') + or extra.get('detail_url') + or extra.get('source_url') + ) + source_url = source_url_raw.strip() if isinstance(source_url_raw, str) else None + if source_url == "": + source_url = None # Get series info for library naming templates series_name = release_data.get('series_name') or extra.get('series_name') @@ -220,6 +230,7 @@ def queue_release( size=release_data.get('size'), preview=preview, content_type=content_type, + source_url=source_url, series_name=series_name, series_position=series_position, subtitle=subtitle, diff --git a/shelfmark/release_sources/audiobookbay/handler.py b/shelfmark/release_sources/audiobookbay/handler.py index 0f79b0f8..ac102f91 100644 --- a/shelfmark/release_sources/audiobookbay/handler.py +++ b/shelfmark/release_sources/audiobookbay/handler.py @@ -19,6 +19,19 @@ logger = setup_logger(__name__) class AudiobookBayHandler(ExternalClientHandler): """Handler for AudiobookBay downloads via configured torrent client.""" + @staticmethod + def _resolve_detail_url(task: DownloadTask) -> Optional[str]: + """Resolve ABB detail URL from queued task metadata.""" + source_url = (task.source_url or "").strip() + if source_url: + return source_url + + # Backward-compat: older tests and some legacy flows used task_id as URL. + task_id = (task.task_id or "").strip() + if task_id.startswith(("http://", "https://")): + return task_id + return None + def _get_client(self, protocol: str) -> Optional[DownloadClient]: """Compatibility shim so module-level patching still works in tests.""" return get_client(protocol) @@ -33,7 +46,12 @@ class AudiobookBayHandler(ExternalClientHandler): status_callback: Callable[[str, Optional[str]], None], ) -> Optional[DownloadRequest]: """Resolve ABB detail page into a magnet-link download request.""" - detail_url = task.task_id + detail_url = self._resolve_detail_url(task) + if not detail_url: + status_callback("error", "Missing AudiobookBay details URL") + logger.warning(f"Missing details URL for AudiobookBay task: {task.task_id}") + return None + hostname = normalize_hostname(config.get("ABB_HOSTNAME", "")) if not hostname: hostname = normalize_hostname(urlparse(detail_url).hostname) diff --git a/tests/audiobookbay/test_handler.py b/tests/audiobookbay/test_handler.py index ac7c54df..9cafdfd4 100644 --- a/tests/audiobookbay/test_handler.py +++ b/tests/audiobookbay/test_handler.py @@ -82,6 +82,43 @@ class TestAudiobookBayHandlerDownload: mock_poll.assert_called_once() assert "resolving" in recorder.statuses + @patch('shelfmark.release_sources.audiobookbay.handler.scraper.extract_magnet_link') + @patch('shelfmark.release_sources.audiobookbay.handler.get_client') + def test_download_uses_source_url_for_hashed_task_id(self, mock_get_client, mock_extract_magnet): + """Test release queue flow where task_id is source hash and source_url has detail URL.""" + mock_extract_magnet.return_value = "magnet:?xt=urn:btih:abc123" + + mock_client = MagicMock() + mock_client.name = "qbittorrent" + mock_client.find_existing.return_value = None + mock_client.add_download.return_value = "download_id_123" + mock_get_client.return_value = mock_client + + handler = AudiobookBayHandler() + task = DownloadTask( + task_id="35f56a3e5734bfa69c3169ee8e605a60", + source="audiobookbay", + title="Test Book", + content_type="audiobook", + source_url="https://audiobookbay.lu/abss/test-book/", + ) + cancel_flag = Event() + recorder = ProgressRecorder() + with patch.object(AudiobookBayHandler, "_poll_and_complete", return_value=None): + result = handler.download( + task=task, + cancel_flag=cancel_flag, + progress_callback=recorder.progress_callback, + status_callback=recorder.status_callback, + ) + + assert result is None + mock_extract_magnet.assert_called_once_with( + "https://audiobookbay.lu/abss/test-book/", + "audiobookbay.lu" + ) + assert "resolving" in recorder.statuses + @patch('shelfmark.release_sources.audiobookbay.handler.scraper.extract_magnet_link') @patch('shelfmark.release_sources.audiobookbay.handler.get_client') def test_download_existing_complete(self, mock_get_client, mock_extract_magnet): diff --git a/tests/download/test_orchestrator_user_output_mode.py b/tests/download/test_orchestrator_user_output_mode.py index ca623525..61c9192e 100644 --- a/tests/download/test_orchestrator_user_output_mode.py +++ b/tests/download/test_orchestrator_user_output_mode.py @@ -74,6 +74,7 @@ def test_queue_release_uses_user_specific_books_output_mode(monkeypatch): "content_type": "book (fiction)", "format": "epub", "size": "1 MB", + "download_url": "https://audiobookbay.lu/abss/release-title/", } success, error = orchestrator.queue_release(release_data, user_id=42, username="alice") @@ -83,6 +84,7 @@ def test_queue_release_uses_user_specific_books_output_mode(monkeypatch): task = captured["task"] assert task.output_mode == "email" assert task.output_args == {"to": "alice@example.com"} + assert task.source_url == "https://audiobookbay.lu/abss/release-title/" assert ("BOOKS_OUTPUT_MODE", 42) in config_calls