Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9cb5557
feat(ui): add book counts to status tabs
djdembeck Feb 17, 2026
3225d2e
fix(i18n): use pluralize filter for grammatical correctness in aria-l…
djdembeck Feb 17, 2026
03f8c2c
fix(ui): prevent description text from being cut off
djdembeck Feb 17, 2026
9fa10d8
feat(search): improve filename normalization for better auto results
djdembeck Feb 18, 2026
4d0c1b0
feat(importer): improve template accessibility and CSS
djdembeck Feb 18, 2026
79ab01f
fix(search): update regex patterns for narrator and year removal
djdembeck Feb 18, 2026
2325908
fix(accessibility): add keyboard navigation for book tabs and fix emp…
djdembeck Feb 18, 2026
156c4c3
Merge branch 'develop' into feature/issue-159-search-string-cleanup
djdembeck Feb 18, 2026
01af28b
fix: accessibility and regex improvements
djdembeck Feb 18, 2026
64a1cf1
fix(book_tabs): add defensive null checks and fix HTML attribute quoting
djdembeck Feb 18, 2026
c10cb28
feat(book_tabs): add keyboard navigation for tabs
djdembeck Feb 18, 2026
f8d3fbb
fix(book_tabs): add user-initiated flag to openTab and null-check tabId
djdembeck Mar 2, 2026
ecc29d3
fix(book_tabs): safer event handling and aria-controls fallback
djdembeck Mar 2, 2026
4c1a8e4
fix(book_tabs): normalize tab IDs and fix aria-controls lookup
djdembeck Mar 2, 2026
d8d180b
fix(tabs): validate tab elements before clearing panes and add compre…
djdembeck Mar 2, 2026
71f9724
fix(tabs): normalize tabId and refactor tests to use initializeTabs
djdembeck Mar 2, 2026
ed5607b
fix(book_tabs): guard against blank tab IDs and prevent duplicate key…
djdembeck Mar 2, 2026
6385d30
fix: trim whitespace before stripping hash in tab ID normalization
djdembeck Mar 2, 2026
b1179fb
fix(book_tabs): add doc parameter to openTab and handleKeyDown for te…
djdembeck Mar 2, 2026
0fdc124
fix(book_tabs): prevent duplicate warnings when data-default normaliz…
djdembeck Mar 2, 2026
4daa75c
fix(book_tabs): refactor fallback tab resolution into helper, handle …
djdembeck Mar 2, 2026
82bb534
fix(book_tabs): validate default tab exists and handle empty aria-con…
djdembeck Mar 3, 2026
35760cc
fix(book_tabs): add defensive guard for event.preventDefault in handl…
djdembeck Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions importer/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@
margin: 0;
}

.book-description {
overflow-wrap: anywhere;
hyphens: auto;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

{% block style %}{% endblock %}
</style>
</head>
Expand Down
2 changes: 1 addition & 1 deletion importer/templates/book_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{% if book.status.status == 'Error' %}
<p>{{ book.status.message }}</p>
{% else %}
<p>
<p class="book-description">
{{ book.long_desc|safe }}
</p>
{% endif %}
Expand Down
14 changes: 7 additions & 7 deletions importer/templates/book_tabs.html
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{% extends "base.html" %}
{% block content %}
<div class="box">
<div class="tabs is-fullwidth is-toggle" style="background: white;" data-default={{ default_view }}>
<div class="tabs is-fullwidth is-toggle" style="background: white;" data-default={{ default_view }} role="tablist">
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
<ul>
<li class="tab is-active" id="done-tab" onclick="openTab('done')"><a>Done</a></li>
<li class="tab" id="processing-tab" onclick="openTab('processing')"><a>Processing</a></li>
<li class="tab" id="error-tab" onclick="openTab('error')"><a>Error</a></li>
<li class="tab is-active" id="done-tab" onclick="openTab('done')"><a role="tab" aria-selected="true" aria-controls="done" aria-label="Done - {{ done_books|length }} book{{ done_books|length|pluralize }}">Done ({{ done_books|length }})</a></li>
<li class="tab" id="processing-tab" onclick="openTab('processing')"><a role="tab" aria-selected="false" aria-controls="processing" aria-label="Processing - {{ processing_books|length }} book{{ processing_books|length|pluralize }}">Processing ({{ processing_books|length }})</a></li>
<li class="tab" id="error-tab" onclick="openTab('error')"><a role="tab" aria-selected="false" aria-controls="error" aria-label="Error - {{ error_books|length }} book{{ error_books|length|pluralize }}">Error ({{ error_books|length }})</a></li>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</ul>
</div>

<div class="tab-content">
<div class="tab-pane" id="done">
<div class="tab-pane" id="done" role="tabpanel" aria-labelledby="done-tab">
{% include "book_list.html" with books=done_books %}
</div>
<div class="tab-pane" id="processing" style="display: none;">
<div class="tab-pane" id="processing" role="tabpanel" aria-labelledby="processing-tab" style="display: none;">
{% include "book_list.html" with books=processing_books %}
</div>
<div class="tab-pane" id="error" style="display: none;">
<div class="tab-pane" id="error" role="tabpanel" aria-labelledby="error-tab" style="display: none;">
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
{% include "book_list.html" with books=error_books %}
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions importer/templates/book_tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ function openTab(tabId) {

document.getElementById(tabId).style.display = "block";
document.getElementById(`${tabId}-tab`).classList.add("is-active");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const tabAnchors = document.querySelectorAll('.tab a[role="tab"]');
tabAnchors.forEach(anchor => {
if (anchor.getAttribute('aria-controls') === tabId) {
anchor.setAttribute('aria-selected', 'true');
} else {
anchor.setAttribute('aria-selected', 'false');
}
});
}

window.addEventListener('load', function () {
Expand Down
16 changes: 16 additions & 0 deletions utils/search_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ def normalize_name(self, name) -> str:
name,
flags=re.IGNORECASE,
)
# Remove "read by" / "narrated by" patterns
name = re.sub(
r"\b(read|narrated)\s+by\s+\w+(?:\s+\w+){1,2}",
"",
name,
flags=re.IGNORECASE,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Remove part/volume/book/chapter indicators
name = re.sub(
r"\b(part|vol|volume|book|chapter)\s*\d+\s*(of\s*\d+)?\b",
"",
name,
flags=re.IGNORECASE,
)
# Remove years
name = re.sub(r"[\(\[]\s*\d{4}\s*[\)\]]|\b(19|20)\d{2}\b", "", name)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
# Remove unwanted whitespaces
name = re.sub(r"\s+", " ", name)
# Remove leading and trailing whitespaces
Expand Down
71 changes: 71 additions & 0 deletions utils/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from importer.models import Book, Setting, Status, StatusChoices
from utils.merge import run_m4b_merge
from utils.search_tools import SearchTool


class TestSubprocessMerge(TestCase):
Expand Down Expand Up @@ -276,3 +277,73 @@ def test_output_path_parsing_failure(self, mock_subprocess_run):

# Verify ValueError was raised with correct message
self.assertIn("Could not parse output path", str(context.exception))


class TestSearchToolNormalizeName(TestCase):
"""Unit tests for SearchTool.normalize_name method."""

def setUp(self):
"""Set up test fixtures."""
self.tool = SearchTool(filename="test")

def test_normalize_name_removes_read_by(self):
"""Verify 'read by' and narrator names are removed."""
result = self.tool.normalize_name("Great Book read by John Smith")
self.assertNotIn("read by", result.lower())
self.assertNotIn("john smith", result.lower())
self.assertIn("great", result.lower())
self.assertIn("book", result.lower())

def test_normalize_name_removes_part_indicators(self):
"""Verify part/volume indicators are removed."""
result = self.tool.normalize_name("Series Book Part 1 of 3")
self.assertNotIn("part 1", result.lower())
self.assertNotIn("of 3", result.lower())
self.assertIn("series", result.lower())
self.assertIn("book", result.lower())

def test_normalize_name_removes_years(self):
"""Verify years in parentheses are removed."""
result = self.tool.normalize_name("Book Title (2020)")
self.assertNotIn("2020", result)
self.assertIn("book", result.lower())
self.assertIn("title", result.lower())

def test_normalize_name_keeps_title_words(self):
"""Verify actual title words are preserved."""
result = self.tool.normalize_name("The Great Gatsby")
self.assertIn("great", result.lower())
self.assertIn("gatsby", result.lower())
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def test_normalize_name_removes_narrated_by(self):
"""Verify 'narrated by' and narrator names are removed."""
result = self.tool.normalize_name("Great Book narrated by Jane Doe")
self.assertNotIn("narrated by", result.lower())
self.assertNotIn("jane doe", result.lower())
self.assertIn("great", result.lower())
self.assertIn("book", result.lower())

def test_normalize_name_removes_standalone_years(self):
"""Verify standalone years are removed but other text preserved."""
result = self.tool.normalize_name("Book Title 2020 Edition")
self.assertNotIn("2020", result)
self.assertIn("book", result.lower())
self.assertIn("title", result.lower())
self.assertIn("edition", result.lower())

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def test_normalize_name_removes_volume_indicators(self):
"""Verify volume/chapter indicators are removed."""
result = self.tool.normalize_name("Epic Saga Volume 2")
self.assertNotIn("volume 2", result.lower())
self.assertNotIn("vol 2", result.lower())
self.assertIn("epic", result.lower())
self.assertIn("saga", result.lower())

def test_normalize_name_preserves_trailing_text_after_narrator(self):
"""Verify trailing text after narrator is preserved."""
result = self.tool.normalize_name("Great Book read by John Smith The Sequel")
self.assertNotIn("read by", result.lower())
self.assertNotIn("john smith", result.lower())
self.assertIn("great", result.lower())
self.assertIn("book", result.lower())
self.assertIn("sequel", result.lower())