mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-06 09:24:40 +01:00
Add Audiobook support to IRC (#1136)
This commit is contained in:
@@ -1395,9 +1395,11 @@ Delay between requests in seconds to avoid rate limiting (0-10).
|
||||
| `IRC_SERVER` | IRC server hostname | string | _none_ |
|
||||
| `IRC_PORT` | IRC server port (usually 6697 for TLS, 6667 for plain) | number | `6697` |
|
||||
| `IRC_USE_TLS` | Enable TLS/SSL encryption for the IRC connection. Disable for servers that don't support TLS. | boolean | `true` |
|
||||
| `IRC_CHANNEL` | Channel name without the # prefix | string | _none_ |
|
||||
| `IRC_CHANNEL` | Channel name without the # prefix. Used for all searches unless a separate audiobook channel is configured below. | string | _none_ |
|
||||
| `IRC_NICK` | Your IRC nickname (required). Must be unique on the IRC network. | string | _none_ |
|
||||
| `IRC_SEARCH_BOT` | The search bot to address queries to (required). | string | _none_ |
|
||||
| `IRC_AUDIOBOOK_CHANNEL` | Optional. Channel name (without the # prefix) to use for audiobook searches. Leave blank to use the main channel above for audiobooks too. | string | _none_ |
|
||||
| `IRC_AUDIOBOOK_SEARCH_BOT` | Optional. Search bot for the audiobook channel. Leave blank to reuse the main search bot above. Only used when an audiobook channel is set. | string | _none_ |
|
||||
| `IRC_CACHE_TTL` | How long to keep cached search results before they expire. | string (choice) | `2592000` |
|
||||
|
||||
<details>
|
||||
@@ -1435,7 +1437,7 @@ Enable TLS/SSL encryption for the IRC connection. Disable for servers that don't
|
||||
|
||||
**Channel**
|
||||
|
||||
Channel name without the # prefix
|
||||
Channel name without the # prefix. Used for all searches unless a separate audiobook channel is configured below.
|
||||
|
||||
- **Type:** string
|
||||
- **Default:** _none_
|
||||
@@ -1461,6 +1463,24 @@ The search bot to address queries to (required). Searches are sent as "@<bot> <q
|
||||
- **Default:** _none_
|
||||
- **Required:** Yes
|
||||
|
||||
#### `IRC_AUDIOBOOK_CHANNEL`
|
||||
|
||||
**Audiobook channel**
|
||||
|
||||
Optional. Channel name (without the # prefix) to use for audiobook searches. Leave blank to use the main channel above for audiobooks too.
|
||||
|
||||
- **Type:** string
|
||||
- **Default:** _none_
|
||||
|
||||
#### `IRC_AUDIOBOOK_SEARCH_BOT`
|
||||
|
||||
**Audiobook search bot**
|
||||
|
||||
Optional. Search bot for the audiobook channel. Leave blank to reuse the main search bot above. Only used when an audiobook channel is set.
|
||||
|
||||
- **Type:** string
|
||||
- **Default:** _none_
|
||||
|
||||
#### `IRC_CACHE_TTL`
|
||||
|
||||
**Cache Duration**
|
||||
|
||||
@@ -72,7 +72,10 @@ def irc_settings() -> list[SettingsField]:
|
||||
key="IRC_CHANNEL",
|
||||
label="Channel",
|
||||
placeholder="e.g. ebooks",
|
||||
description="Channel name without the # prefix",
|
||||
description=(
|
||||
"Channel name without the # prefix. Used for all searches unless a "
|
||||
"separate audiobook channel is configured below."
|
||||
),
|
||||
required=True,
|
||||
env_supported=True,
|
||||
),
|
||||
@@ -95,6 +98,38 @@ def irc_settings() -> list[SettingsField]:
|
||||
required=True,
|
||||
env_supported=True,
|
||||
),
|
||||
HeadingField(
|
||||
key="audiobook_heading",
|
||||
title="Audiobooks",
|
||||
description=(
|
||||
"Some networks index audiobooks in a separate channel from ebooks "
|
||||
"(for example #ebooks for ebooks and #bookz for audiobooks). "
|
||||
"Configure that channel here to search it for audiobook requests. "
|
||||
"Leave these blank to search the main channel above for both."
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
key="IRC_AUDIOBOOK_CHANNEL",
|
||||
label="Audiobook channel",
|
||||
placeholder="e.g. bookz",
|
||||
description=(
|
||||
"Optional. Channel name (without the # prefix) to use for audiobook "
|
||||
"searches. Leave blank to use the main channel above for audiobooks too."
|
||||
),
|
||||
required=False,
|
||||
env_supported=True,
|
||||
),
|
||||
TextField(
|
||||
key="IRC_AUDIOBOOK_SEARCH_BOT",
|
||||
label="Audiobook search bot",
|
||||
placeholder="e.g. search",
|
||||
description=(
|
||||
"Optional. Search bot for the audiobook channel. Leave blank to reuse "
|
||||
"the main search bot above. Only used when an audiobook channel is set."
|
||||
),
|
||||
required=False,
|
||||
env_supported=True,
|
||||
),
|
||||
HeadingField(
|
||||
key="cache_heading",
|
||||
title="Search Cache",
|
||||
|
||||
@@ -240,6 +240,19 @@ class IRCReleaseSource(ReleaseSource):
|
||||
nick = _config_text("IRC_NICK")
|
||||
search_bot = _config_text("IRC_SEARCH_BOT")
|
||||
|
||||
# Audiobooks may be indexed in a separate channel from ebooks on some networks
|
||||
# (e.g. #ebooks for ebooks, #bookz for audiobooks). When an audiobook channel is
|
||||
# configured and an audiobook was requested, route the search there (with its own
|
||||
# search bot if set). Otherwise fall back to the main channel/bot, which keeps the
|
||||
# single-channel networks that index both formats working unchanged.
|
||||
if is_audiobook(content_type):
|
||||
audiobook_channel = _config_text("IRC_AUDIOBOOK_CHANNEL")
|
||||
if audiobook_channel:
|
||||
channel = audiobook_channel
|
||||
audiobook_search_bot = _config_text("IRC_AUDIOBOOK_SEARCH_BOT")
|
||||
if audiobook_search_bot:
|
||||
search_bot = audiobook_search_bot
|
||||
|
||||
# Never post an unaddressed query to the channel. A bare book title looks like
|
||||
# spam to everyone else in the channel and gets the nick banned. Searches must
|
||||
# be addressed to a search bot ("@<bot> <query>").
|
||||
|
||||
@@ -161,6 +161,145 @@ def test_search_no_dcc_offer_releases_connection_and_caches_empty_result(monkeyp
|
||||
]
|
||||
|
||||
|
||||
def test_audiobook_search_routes_to_configured_audiobook_channel_and_bot(monkeypatch):
|
||||
"""An audiobook request uses the audiobook channel/bot when configured."""
|
||||
import shelfmark.release_sources.irc.source as irc_source
|
||||
|
||||
source = IRCReleaseSource()
|
||||
connection_kwargs: dict[str, object] = {}
|
||||
cache_calls: list[dict[str, object]] = []
|
||||
|
||||
class FakeClient:
|
||||
online_servers: set[str] = set()
|
||||
|
||||
def send_message(self, channel: str, message: str) -> None:
|
||||
self.channel = channel
|
||||
self.message = message
|
||||
|
||||
def wait_for_dcc(
|
||||
self, *, timeout: float, result_type: bool, expected_senders: object = None
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
client = FakeClient()
|
||||
|
||||
monkeypatch.setattr(
|
||||
irc_source,
|
||||
"_config_text",
|
||||
lambda key: {
|
||||
"IRC_SERVER": "irc.example.net",
|
||||
"IRC_CHANNEL": "ebooks",
|
||||
"IRC_NICK": "tester",
|
||||
"IRC_SEARCH_BOT": "search",
|
||||
"IRC_AUDIOBOOK_CHANNEL": "bookz",
|
||||
"IRC_AUDIOBOOK_SEARCH_BOT": "audiosearch",
|
||||
}.get(key, ""),
|
||||
)
|
||||
irc_source._recent_message_sends.clear()
|
||||
|
||||
monkeypatch.setattr(source, "is_available", lambda: True)
|
||||
monkeypatch.setattr(irc_source, "_enforce_rate_limit", lambda: None)
|
||||
monkeypatch.setattr(irc_source, "_emit_status", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.irc.cache.get_cached_results",
|
||||
lambda cache_key, *_args, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.irc.cache.cache_results",
|
||||
lambda cache_key, title, releases, *, online_servers=None: cache_calls.append(
|
||||
{"cache_key": cache_key}
|
||||
),
|
||||
)
|
||||
|
||||
def fake_get_connection(**kwargs: object) -> FakeClient:
|
||||
connection_kwargs.update(kwargs)
|
||||
return client
|
||||
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.irc.connection_manager.connection_manager.get_connection",
|
||||
fake_get_connection,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.irc.connection_manager.connection_manager.release_connection",
|
||||
lambda _client: None,
|
||||
)
|
||||
|
||||
book = BookMetadata(provider="hardcover", provider_id="ab", title="Audio Book")
|
||||
plan = SimpleNamespace(primary_query="Audio Book")
|
||||
|
||||
source.search(book, plan, content_type="audiobook")
|
||||
|
||||
# Joined the audiobook channel, addressed the audiobook search bot, and the cache
|
||||
# key is namespaced to the audiobook channel (so it won't collide with the ebook one).
|
||||
assert connection_kwargs["channel"] == "bookz"
|
||||
assert client.channel == "#bookz"
|
||||
assert client.message == "@audiosearch Audio Book"
|
||||
assert cache_calls == [{"cache_key": "irc.example.net:bookz:audio book"}]
|
||||
|
||||
|
||||
def test_audiobook_search_reuses_main_bot_when_only_channel_configured(monkeypatch):
|
||||
"""With an audiobook channel but no audiobook bot, reuse the main search bot."""
|
||||
import shelfmark.release_sources.irc.source as irc_source
|
||||
|
||||
source = IRCReleaseSource()
|
||||
|
||||
class FakeClient:
|
||||
online_servers: set[str] = set()
|
||||
|
||||
def send_message(self, channel: str, message: str) -> None:
|
||||
self.channel = channel
|
||||
self.message = message
|
||||
|
||||
def wait_for_dcc(
|
||||
self, *, timeout: float, result_type: bool, expected_senders: object = None
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
client = FakeClient()
|
||||
|
||||
monkeypatch.setattr(
|
||||
irc_source,
|
||||
"_config_text",
|
||||
lambda key: {
|
||||
"IRC_SERVER": "irc.example.net",
|
||||
"IRC_CHANNEL": "ebooks",
|
||||
"IRC_NICK": "tester",
|
||||
"IRC_SEARCH_BOT": "search",
|
||||
"IRC_AUDIOBOOK_CHANNEL": "bookz",
|
||||
"IRC_AUDIOBOOK_SEARCH_BOT": "",
|
||||
}.get(key, ""),
|
||||
)
|
||||
irc_source._recent_message_sends.clear()
|
||||
|
||||
monkeypatch.setattr(source, "is_available", lambda: True)
|
||||
monkeypatch.setattr(irc_source, "_enforce_rate_limit", lambda: None)
|
||||
monkeypatch.setattr(irc_source, "_emit_status", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.irc.cache.get_cached_results",
|
||||
lambda cache_key, *_args, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.irc.cache.cache_results",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.irc.connection_manager.connection_manager.get_connection",
|
||||
lambda **_kwargs: client,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.irc.connection_manager.connection_manager.release_connection",
|
||||
lambda _client: None,
|
||||
)
|
||||
|
||||
book = BookMetadata(provider="hardcover", provider_id="ab2", title="Audio Book")
|
||||
plan = SimpleNamespace(primary_query="Audio Book")
|
||||
|
||||
source.search(book, plan, content_type="audiobook")
|
||||
|
||||
assert client.channel == "#bookz"
|
||||
assert client.message == "@search Audio Book"
|
||||
|
||||
|
||||
def test_search_without_search_bot_never_posts_to_channel(monkeypatch):
|
||||
"""A bare (unaddressed) query must never reach the channel; refuse to connect."""
|
||||
import shelfmark.release_sources.irc.source as irc_source
|
||||
|
||||
Reference in New Issue
Block a user