df
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
This commit is contained in:
@@ -23,24 +23,25 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class SearchModal(ModalScreen):
|
||||
"""Modal screen for searching OpenLibrary and Soulseek."""
|
||||
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
Binding("enter", "search_focused", "Search"),
|
||||
Binding("ctrl+t", "scrape_tags", "Scrape Tags"),
|
||||
]
|
||||
|
||||
|
||||
CSS_PATH = "search.tcss"
|
||||
|
||||
|
||||
class SearchSelected(Message):
|
||||
"""Posted when user selects a search result."""
|
||||
|
||||
def __init__(self, result: dict) -> None:
|
||||
self.result = result
|
||||
super().__init__()
|
||||
|
||||
|
||||
def __init__(self, app_instance=None):
|
||||
"""Initialize the search modal.
|
||||
|
||||
|
||||
Args:
|
||||
app_instance: Reference to the main App instance for worker creation
|
||||
"""
|
||||
@@ -55,108 +56,98 @@ class SearchModal(ModalScreen):
|
||||
self.current_result_table: Optional[ResultTable] = None
|
||||
self.is_searching = False
|
||||
self.current_worker = None # Track worker for search operations
|
||||
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create child widgets for the search modal."""
|
||||
with Vertical(id="search-container"):
|
||||
yield Static("Search Books & Music", id="search-title")
|
||||
|
||||
|
||||
with Horizontal(id="search-controls"):
|
||||
# Source selector
|
||||
self.source_select = Select(
|
||||
[("OpenLibrary", "openlibrary"), ("Soulseek", "soulseek")],
|
||||
value="openlibrary",
|
||||
id="source-select"
|
||||
id="source-select",
|
||||
)
|
||||
yield self.source_select
|
||||
|
||||
|
||||
# Search input
|
||||
self.search_input = Input(
|
||||
placeholder="Enter search query...",
|
||||
id="search-input"
|
||||
)
|
||||
self.search_input = Input(placeholder="Enter search query...", id="search-input")
|
||||
yield self.search_input
|
||||
|
||||
|
||||
# Search button
|
||||
yield Button("Search", id="search-button", variant="primary")
|
||||
|
||||
|
||||
# Results table
|
||||
self.results_table = DataTable(id="results-table")
|
||||
yield self.results_table
|
||||
|
||||
|
||||
# Two-column layout: tags on left, source/submit on right
|
||||
with Horizontal(id="bottom-controls"):
|
||||
# Left column: Tags textarea
|
||||
with Vertical(id="tags-column"):
|
||||
self.tags_textarea = TextArea(
|
||||
text="",
|
||||
id="result-tags-textarea",
|
||||
read_only=False
|
||||
text="", id="result-tags-textarea", read_only=False
|
||||
)
|
||||
self.tags_textarea.border_title = "Tags [Ctrl+T: Scrape]"
|
||||
yield self.tags_textarea
|
||||
|
||||
|
||||
# Right column: Library source and submit button
|
||||
with Vertical(id="source-submit-column"):
|
||||
# Library source selector (for OpenLibrary results)
|
||||
self.library_source_select = Select(
|
||||
[("Local", "local"), ("Download", "download")],
|
||||
value="local",
|
||||
id="library-source-select"
|
||||
id="library-source-select",
|
||||
)
|
||||
yield self.library_source_select
|
||||
|
||||
|
||||
# Submit button
|
||||
yield Button("Submit", id="submit-button", variant="primary")
|
||||
|
||||
|
||||
# Buttons at bottom
|
||||
with Horizontal(id="search-buttons"):
|
||||
yield Button("Select", id="select-button", variant="primary")
|
||||
yield Button("Download", id="download-button", variant="primary")
|
||||
yield Button("Cancel", id="cancel-button", variant="default")
|
||||
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Set up the table columns and focus."""
|
||||
# Set up results table columns
|
||||
self.results_table.add_columns(
|
||||
"Title",
|
||||
"Author/Artist",
|
||||
"Year/Album",
|
||||
"Details"
|
||||
)
|
||||
|
||||
self.results_table.add_columns("Title", "Author/Artist", "Year/Album", "Details")
|
||||
|
||||
# Focus on search input
|
||||
self.search_input.focus()
|
||||
|
||||
|
||||
async def _perform_search(self) -> None:
|
||||
"""Perform the actual search based on selected source."""
|
||||
if not self.search_input or not self.source_select or not self.results_table:
|
||||
logger.error("[search-modal] Widgets not initialized")
|
||||
return
|
||||
|
||||
|
||||
query = self.search_input.value.strip()
|
||||
if not query:
|
||||
logger.warning("[search-modal] Empty search query")
|
||||
return
|
||||
|
||||
|
||||
source = self.source_select.value
|
||||
|
||||
|
||||
# Clear existing results
|
||||
self.results_table.clear(columns=True)
|
||||
self.current_results = []
|
||||
self.current_result_table = None
|
||||
|
||||
|
||||
self.is_searching = True
|
||||
|
||||
|
||||
# Create worker for tracking
|
||||
if self.app_instance and hasattr(self.app_instance, 'create_worker'):
|
||||
if self.app_instance and hasattr(self.app_instance, "create_worker"):
|
||||
self.current_worker = self.app_instance.create_worker(
|
||||
source,
|
||||
title=f"{source.capitalize()} Search: {query[:40]}",
|
||||
description=f"Searching {source} for: {query}"
|
||||
description=f"Searching {source} for: {query}",
|
||||
)
|
||||
self.current_worker.log_step(f"Connecting to {source}...")
|
||||
|
||||
|
||||
try:
|
||||
provider = get_search_provider(source)
|
||||
if not provider:
|
||||
@@ -168,10 +159,10 @@ class SearchModal(ModalScreen):
|
||||
logger.info(f"[search-modal] Searching {source} for: {query}")
|
||||
results = provider.search(query, limit=20)
|
||||
self.current_results = results
|
||||
|
||||
|
||||
if self.current_worker:
|
||||
self.current_worker.log_step(f"Found {len(results)} results")
|
||||
|
||||
|
||||
# Create ResultTable
|
||||
table = ResultTable(f"Search Results: {query}")
|
||||
for res in results:
|
||||
@@ -183,10 +174,16 @@ class SearchModal(ModalScreen):
|
||||
else:
|
||||
# Fallback if no columns defined
|
||||
row.add_column("Title", res.title)
|
||||
row.add_column("Target", getattr(res, 'path', None) or getattr(res, 'url', None) or getattr(res, 'target', None) or '')
|
||||
|
||||
row.add_column(
|
||||
"Target",
|
||||
getattr(res, "path", None)
|
||||
or getattr(res, "url", None)
|
||||
or getattr(res, "target", None)
|
||||
or "",
|
||||
)
|
||||
|
||||
self.current_result_table = table
|
||||
|
||||
|
||||
# Populate UI
|
||||
if table.rows:
|
||||
# Add headers
|
||||
@@ -198,27 +195,27 @@ class SearchModal(ModalScreen):
|
||||
else:
|
||||
self.results_table.add_columns("Message")
|
||||
self.results_table.add_row("No results found")
|
||||
|
||||
|
||||
# Finish worker
|
||||
if self.current_worker:
|
||||
self.current_worker.finish("completed", f"Found {len(results)} results")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[search-modal] Search error: {e}", exc_info=True)
|
||||
if self.current_worker:
|
||||
self.current_worker.finish("error", f"Search failed: {str(e)}")
|
||||
|
||||
|
||||
finally:
|
||||
self.is_searching = False
|
||||
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses."""
|
||||
button_id = event.button.id
|
||||
|
||||
|
||||
if button_id == "search-button":
|
||||
# Run search asynchronously
|
||||
asyncio.create_task(self._perform_search())
|
||||
|
||||
|
||||
elif button_id == "select-button":
|
||||
# Get selected row and populate tags textarea
|
||||
if self.results_table and self.results_table.row_count > 0:
|
||||
@@ -229,7 +226,7 @@ class SearchModal(ModalScreen):
|
||||
self._populate_tags_from_result(result)
|
||||
else:
|
||||
logger.warning("[search-modal] No results to select")
|
||||
|
||||
|
||||
elif button_id == "download-button":
|
||||
# Download the selected result
|
||||
if self.current_results and self.results_table.row_count > 0:
|
||||
@@ -239,67 +236,75 @@ class SearchModal(ModalScreen):
|
||||
if getattr(result, "table", "") == "openlibrary":
|
||||
asyncio.create_task(self._download_book(result))
|
||||
else:
|
||||
logger.warning("[search-modal] Download only supported for OpenLibrary results")
|
||||
logger.warning(
|
||||
"[search-modal] Download only supported for OpenLibrary results"
|
||||
)
|
||||
else:
|
||||
logger.warning("[search-modal] No result selected for download")
|
||||
|
||||
|
||||
elif button_id == "submit-button":
|
||||
# Submit the current result with tags and source
|
||||
if self.current_results and self.results_table.row_count > 0:
|
||||
selected_row = self.results_table.cursor_row
|
||||
if 0 <= selected_row < len(self.current_results):
|
||||
result = self.current_results[selected_row]
|
||||
|
||||
|
||||
# Convert to dict if needed for submission
|
||||
if hasattr(result, 'to_dict'):
|
||||
if hasattr(result, "to_dict"):
|
||||
result_dict = result.to_dict()
|
||||
else:
|
||||
result_dict = result
|
||||
|
||||
|
||||
# Get tags from textarea
|
||||
tags_text = self.tags_textarea.text if self.tags_textarea else ""
|
||||
# Get library source (if OpenLibrary)
|
||||
library_source = self.library_source_select.value if self.library_source_select else "local"
|
||||
|
||||
library_source = (
|
||||
self.library_source_select.value if self.library_source_select else "local"
|
||||
)
|
||||
|
||||
# Add tags and source to result
|
||||
result_dict["tags_text"] = tags_text
|
||||
result_dict["library_source"] = library_source
|
||||
|
||||
|
||||
# Post message and dismiss
|
||||
self.post_message(self.SearchSelected(result_dict))
|
||||
self.dismiss(result_dict)
|
||||
else:
|
||||
logger.warning("[search-modal] No result selected for submission")
|
||||
|
||||
|
||||
elif button_id == "cancel-button":
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
def _populate_tags_from_result(self, result: Any) -> None:
|
||||
"""Populate the tags textarea from a selected result."""
|
||||
if not self.tags_textarea:
|
||||
return
|
||||
|
||||
|
||||
# Handle both SearchResult objects and dicts
|
||||
if hasattr(result, 'full_metadata'):
|
||||
if hasattr(result, "full_metadata"):
|
||||
metadata = result.full_metadata or {}
|
||||
source = result.table
|
||||
title = result.title
|
||||
else:
|
||||
# Handle dict (legacy or from to_dict)
|
||||
if 'full_metadata' in result:
|
||||
metadata = result['full_metadata'] or {}
|
||||
elif 'raw_data' in result:
|
||||
metadata = result['raw_data'] or {}
|
||||
if "full_metadata" in result:
|
||||
metadata = result["full_metadata"] or {}
|
||||
elif "raw_data" in result:
|
||||
metadata = result["raw_data"] or {}
|
||||
else:
|
||||
metadata = result
|
||||
|
||||
source = result.get('table', '')
|
||||
title = result.get('title', '')
|
||||
|
||||
|
||||
source = result.get("table", "")
|
||||
title = result.get("title", "")
|
||||
|
||||
# Format tags based on result source
|
||||
if source == "openlibrary":
|
||||
# For OpenLibrary: title, author, year
|
||||
author = ", ".join(metadata.get("authors", [])) if isinstance(metadata.get("authors"), list) else metadata.get("authors", "")
|
||||
author = (
|
||||
", ".join(metadata.get("authors", []))
|
||||
if isinstance(metadata.get("authors"), list)
|
||||
else metadata.get("authors", "")
|
||||
)
|
||||
year = str(metadata.get("year", ""))
|
||||
tags = []
|
||||
if title:
|
||||
@@ -325,10 +330,10 @@ class SearchModal(ModalScreen):
|
||||
# Generic fallback
|
||||
tags = [title]
|
||||
tags_text = "\n".join(tags)
|
||||
|
||||
|
||||
self.tags_textarea.text = tags_text
|
||||
logger.info(f"[search-modal] Populated tags textarea from result")
|
||||
|
||||
|
||||
async def _download_book(self, result: Any) -> None:
|
||||
"""Download a book from OpenLibrary using the provider."""
|
||||
if getattr(result, "table", "") != "openlibrary":
|
||||
@@ -355,12 +360,12 @@ class SearchModal(ModalScreen):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[search-modal] Download error: {e}", exc_info=True)
|
||||
|
||||
|
||||
def action_search_focused(self) -> None:
|
||||
"""Action for Enter key - only search if search input is focused."""
|
||||
if self.search_input and self.search_input.has_focus and not self.is_searching:
|
||||
asyncio.create_task(self._perform_search())
|
||||
|
||||
|
||||
def action_scrape_tags(self) -> None:
|
||||
"""Action for Ctrl+T - populate tags from selected result."""
|
||||
if self.current_results and self.results_table and self.results_table.row_count > 0:
|
||||
@@ -369,18 +374,20 @@ class SearchModal(ModalScreen):
|
||||
if 0 <= selected_row < len(self.current_results):
|
||||
result = self.current_results[selected_row]
|
||||
self._populate_tags_from_result(result)
|
||||
logger.info(f"[search-modal] Ctrl+T: Populated tags from result at row {selected_row}")
|
||||
logger.info(
|
||||
f"[search-modal] Ctrl+T: Populated tags from result at row {selected_row}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"[search-modal] Ctrl+T: Invalid row index {selected_row}")
|
||||
except Exception as e:
|
||||
logger.error(f"[search-modal] Ctrl+T error: {e}")
|
||||
else:
|
||||
logger.warning("[search-modal] Ctrl+T: No results selected")
|
||||
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Action for Escape key - close modal."""
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
"""Handle Enter key in search input - only trigger search here."""
|
||||
if event.input.id == "search-input":
|
||||
|
||||
Reference in New Issue
Block a user