jjlj
This commit is contained in:
@@ -69,33 +69,34 @@ class ExportModal(ModalScreen):
|
||||
"""
|
||||
ext_lower = ext.lower() if ext else ''
|
||||
|
||||
# Audio formats
|
||||
audio_exts = {'.mp3', '.flac', '.wav', '.aac', '.ogg', '.m4a', '.wma', '.opus', '.mka'}
|
||||
audio_formats = [("MKA", "mka"), ("MP3", "mp3"), ("M4A", "m4a"), ("FLAC", "flac"), ("WAV", "wav"), ("AAC", "aac"), ("OGG", "ogg"), ("Opus", "opus")]
|
||||
from helper.utils_constant import mime_maps
|
||||
|
||||
# Video formats (can have audio too)
|
||||
video_exts = {'.mp4', '.mkv', '.webm', '.avi', '.mov', '.flv', '.wmv', '.m4v', '.ts', '.mpg', '.mpeg'}
|
||||
video_formats = [("MP4", "mp4"), ("MKV", "mkv"), ("WebM", "webm"), ("AVI", "avi"), ("MOV", "mov")]
|
||||
found_type = "unknown"
|
||||
|
||||
# Image formats
|
||||
image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.ico'}
|
||||
image_formats = [("JPG", "jpg"), ("PNG", "png"), ("WebP", "webp"), ("GIF", "gif"), ("BMP", "bmp")]
|
||||
# Find type based on extension
|
||||
for category, formats in mime_maps.items():
|
||||
for fmt_key, fmt_info in formats.items():
|
||||
if fmt_info.get("ext") == ext_lower:
|
||||
found_type = category
|
||||
break
|
||||
if found_type != "unknown":
|
||||
break
|
||||
|
||||
# Document formats - no conversion for now
|
||||
document_exts = {'.pdf', '.epub', '.txt', '.docx', '.doc', '.rtf', '.md', '.html', '.mobi', '.cbz', '.cbr'}
|
||||
document_formats = []
|
||||
# Build format options for the found type
|
||||
format_options = []
|
||||
|
||||
if ext_lower in audio_exts:
|
||||
return ('audio', audio_formats)
|
||||
elif ext_lower in video_exts:
|
||||
return ('video', video_formats)
|
||||
elif ext_lower in image_exts:
|
||||
return ('image', image_formats)
|
||||
elif ext_lower in document_exts:
|
||||
return ('document', document_formats)
|
||||
else:
|
||||
# Default to audio if unknown
|
||||
return ('unknown', audio_formats)
|
||||
# If unknown, fallback to audio (matching legacy behavior)
|
||||
target_type = found_type if found_type in mime_maps else "audio"
|
||||
|
||||
if target_type in mime_maps:
|
||||
# Sort formats alphabetically
|
||||
sorted_formats = sorted(mime_maps[target_type].items())
|
||||
for fmt_key, fmt_info in sorted_formats:
|
||||
label = fmt_key.upper()
|
||||
value = fmt_key
|
||||
format_options.append((label, value))
|
||||
|
||||
return (target_type, format_options)
|
||||
|
||||
def _get_library_options(self) -> list:
|
||||
"""Get available library options from config.json."""
|
||||
|
||||
@@ -15,6 +15,8 @@ import asyncio
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from config import load_config
|
||||
from result_table import ResultTable
|
||||
from helper.search_provider import get_provider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,7 +51,8 @@ class SearchModal(ModalScreen):
|
||||
self.results_table: Optional[DataTable] = None
|
||||
self.tags_textarea: Optional[TextArea] = None
|
||||
self.library_source_select: Optional[Select] = None
|
||||
self.current_results: List[dict] = []
|
||||
self.current_results: List[Any] = [] # List of SearchResult objects
|
||||
self.current_result_table: Optional[ResultTable] = None
|
||||
self.is_searching = False
|
||||
self.current_worker = None # Track worker for search operations
|
||||
|
||||
@@ -125,124 +128,6 @@ class SearchModal(ModalScreen):
|
||||
# Focus on search input
|
||||
self.search_input.focus()
|
||||
|
||||
async def _search_openlibrary(self, query: str) -> List[dict]:
|
||||
"""Search OpenLibrary for books."""
|
||||
try:
|
||||
from helper.search_provider import get_provider
|
||||
|
||||
logger.info(f"[search-modal] Searching OpenLibrary for: {query}")
|
||||
|
||||
# Get the OpenLibrary provider (now has smart search built-in)
|
||||
provider = get_provider("openlibrary")
|
||||
if not provider:
|
||||
logger.error("[search-modal] OpenLibrary provider not available")
|
||||
return []
|
||||
|
||||
# Search using the provider (smart search is now default)
|
||||
search_results = provider.search(query, limit=20)
|
||||
|
||||
formatted_results = []
|
||||
for result in search_results:
|
||||
# Extract metadata from SearchResult.full_metadata
|
||||
metadata = result.full_metadata or {}
|
||||
|
||||
formatted_results.append({
|
||||
"title": result.title,
|
||||
"author": ", ".join(metadata.get("authors", [])) if metadata.get("authors") else "Unknown",
|
||||
"year": metadata.get("year", ""),
|
||||
"publisher": metadata.get("publisher", ""),
|
||||
"isbn": metadata.get("isbn", ""),
|
||||
"oclc": metadata.get("oclc", ""),
|
||||
"lccn": metadata.get("lccn", ""),
|
||||
"openlibrary_id": metadata.get("olid", ""),
|
||||
"pages": metadata.get("pages", ""),
|
||||
"language": metadata.get("language", ""),
|
||||
"source": "openlibrary",
|
||||
"columns": result.columns,
|
||||
"raw_data": metadata
|
||||
})
|
||||
|
||||
logger.info(f"[search-modal] Found {len(formatted_results)} OpenLibrary results")
|
||||
return formatted_results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[search-modal] OpenLibrary search error: {e}", exc_info=True)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
async def _search_soulseek(self, query: str) -> List[dict]:
|
||||
"""Search Soulseek for music with automatic worker tracking."""
|
||||
try:
|
||||
from helper.search_provider import get_provider
|
||||
|
||||
# Create worker for tracking
|
||||
worker = None
|
||||
if self.app_instance and hasattr(self.app_instance, 'create_worker'):
|
||||
worker = self.app_instance.create_worker(
|
||||
'soulseek',
|
||||
title=f"Soulseek Search: {query[:40]}",
|
||||
description=f"Searching P2P network for music"
|
||||
)
|
||||
self.current_worker = worker
|
||||
|
||||
if worker:
|
||||
worker.log_step("Connecting to Soulseek peer network...")
|
||||
|
||||
logger.info(f"[search-modal] Searching Soulseek for: {query}")
|
||||
provider = get_provider("soulseek")
|
||||
search_results = provider.search(query, limit=20)
|
||||
|
||||
if worker:
|
||||
worker.log_step(f"Search returned {len(search_results)} results")
|
||||
|
||||
logger.info(f"[search-modal] Found {len(search_results)} Soulseek results")
|
||||
|
||||
# Format results for display
|
||||
formatted_results = []
|
||||
for idx, result in enumerate(search_results):
|
||||
metadata = result.full_metadata or {}
|
||||
artist = metadata.get('artist', '')
|
||||
album = metadata.get('album', '')
|
||||
title = result.title
|
||||
track_num = metadata.get('track_num', '')
|
||||
size_bytes = result.size_bytes or 0
|
||||
|
||||
# Format size as human-readable
|
||||
if size_bytes > 1024 * 1024:
|
||||
size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
elif size_bytes > 1024:
|
||||
size_str = f"{size_bytes / 1024:.1f} KB"
|
||||
else:
|
||||
size_str = f"{size_bytes} B"
|
||||
|
||||
# Build columns for display
|
||||
columns = [
|
||||
("#", str(idx + 1)),
|
||||
("Title", title[:50] if title else "Unknown"),
|
||||
("Artist", artist[:30] if artist else "(no artist)"),
|
||||
("Album", album[:30] if album else ""),
|
||||
]
|
||||
|
||||
formatted_results.append({
|
||||
"title": title if title else "Unknown",
|
||||
"artist": artist if artist else "(no artist)",
|
||||
"album": album,
|
||||
"track": track_num,
|
||||
"filesize": size_str,
|
||||
"bitrate": "", # Not available in Soulseek results
|
||||
"source": "soulseek",
|
||||
"columns": columns,
|
||||
"raw_data": result.to_dict()
|
||||
})
|
||||
|
||||
return formatted_results
|
||||
except Exception as e:
|
||||
logger.error(f"[search-modal] Soulseek search error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
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:
|
||||
@@ -257,87 +142,69 @@ class SearchModal(ModalScreen):
|
||||
source = self.source_select.value
|
||||
|
||||
# Clear existing results
|
||||
self.results_table.clear()
|
||||
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'):
|
||||
self.current_worker = self.app_instance.create_worker(
|
||||
source,
|
||||
title=f"{source.capitalize()} Search: {query[:40]}",
|
||||
description=f"Searching {source} for: {query}"
|
||||
)
|
||||
self.current_worker.log_step(f"Connecting to {source}...")
|
||||
|
||||
try:
|
||||
if source == "openlibrary":
|
||||
results = await self._search_openlibrary(query)
|
||||
elif source == "soulseek":
|
||||
results = await self._search_soulseek(query)
|
||||
else:
|
||||
logger.warning(f"[search-modal] Unknown source: {source}")
|
||||
provider = get_provider(source)
|
||||
if not provider:
|
||||
logger.error(f"[search-modal] Provider not available: {source}")
|
||||
if self.current_worker:
|
||||
self.current_worker.finish("error", "Unknown search source")
|
||||
self.current_worker.finish("error", f"Provider not available: {source}")
|
||||
return
|
||||
|
||||
|
||||
logger.info(f"[search-modal] Searching {source} for: {query}")
|
||||
results = provider.search(query, limit=20)
|
||||
self.current_results = results
|
||||
|
||||
# Populate table with results
|
||||
if results:
|
||||
# Check if first result has columns field
|
||||
first_result = results[0]
|
||||
if "columns" in first_result and first_result["columns"]:
|
||||
# Use dynamic columns from result
|
||||
# Clear existing columns and rebuild based on result columns
|
||||
self.results_table.clear()
|
||||
|
||||
# Extract column headers from first result's columns field
|
||||
column_headers = [col[0] for col in first_result["columns"]]
|
||||
|
||||
# Remove existing columns (we'll readd them with the right headers)
|
||||
# Note: This is a workaround since Textual's DataTable doesn't support dynamic column management well
|
||||
# For now, we just use the dynamic column headers from the result
|
||||
logger.info(f"[search-modal] Using dynamic columns: {column_headers}")
|
||||
|
||||
# Populate rows using the column order from results
|
||||
for result in results:
|
||||
if "columns" in result and result["columns"]:
|
||||
# Extract values in column order
|
||||
row_data = [col[1] for col in result["columns"]]
|
||||
self.results_table.add_row(*row_data)
|
||||
else:
|
||||
# Fallback for results without columns
|
||||
logger.warning(f"[search-modal] Result missing columns field: {result.get('title', 'Unknown')}")
|
||||
else:
|
||||
# Fallback to original hardcoded behavior if columns not available
|
||||
logger.info("[search-modal] No dynamic columns found, using default formatting")
|
||||
|
||||
for result in results:
|
||||
if source == "openlibrary":
|
||||
# Format OpenLibrary results (original hardcoded)
|
||||
year = str(result.get("year", ""))[:4] if result.get("year") else ""
|
||||
details = f"ISBN: {result.get('isbn', '')}" if result.get('isbn') else ""
|
||||
if result.get('openlibrary_id'):
|
||||
details += f" | OL: {result.get('openlibrary_id')}"
|
||||
|
||||
row_data = [
|
||||
result["title"][:60],
|
||||
result["author"][:35],
|
||||
year,
|
||||
details[:40]
|
||||
]
|
||||
else: # soulseek
|
||||
row_data = [
|
||||
result["title"][:50],
|
||||
result["artist"][:30],
|
||||
result["album"][:30],
|
||||
result['filesize']
|
||||
]
|
||||
|
||||
self.results_table.add_row(*row_data)
|
||||
else:
|
||||
# Add a "no results" message
|
||||
self.results_table.add_row("No results found", "", "", "")
|
||||
if self.current_worker:
|
||||
self.current_worker.log_step(f"Found {len(results)} results")
|
||||
|
||||
# Finish worker if tracking
|
||||
# Create ResultTable
|
||||
table = ResultTable(f"Search Results: {query}")
|
||||
for res in results:
|
||||
row = table.add_row()
|
||||
# Add columns from result.columns
|
||||
if res.columns:
|
||||
for name, value in res.columns:
|
||||
row.add_column(name, value)
|
||||
else:
|
||||
# Fallback if no columns defined
|
||||
row.add_column("Title", res.title)
|
||||
row.add_column("Target", res.target)
|
||||
|
||||
self.current_result_table = table
|
||||
|
||||
# Populate UI
|
||||
if table.rows:
|
||||
# Add headers
|
||||
headers = [col.name for col in table.rows[0].columns]
|
||||
self.results_table.add_columns(*headers)
|
||||
# Add rows
|
||||
for row_vals in table.to_datatable_rows():
|
||||
self.results_table.add_row(*row_vals)
|
||||
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}")
|
||||
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)}")
|
||||
|
||||
@@ -382,35 +249,58 @@ class SearchModal(ModalScreen):
|
||||
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'):
|
||||
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"
|
||||
|
||||
# Add tags and source to result
|
||||
result["tags_text"] = tags_text
|
||||
result["library_source"] = library_source
|
||||
result_dict["tags_text"] = tags_text
|
||||
result_dict["library_source"] = library_source
|
||||
|
||||
# Post message and dismiss
|
||||
self.post_message(self.SearchSelected(result))
|
||||
self.dismiss(result)
|
||||
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: dict) -> 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'):
|
||||
metadata = result.full_metadata or {}
|
||||
source = result.origin
|
||||
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 {}
|
||||
else:
|
||||
metadata = result
|
||||
|
||||
source = result.get('origin', result.get('source', ''))
|
||||
title = result.get('title', '')
|
||||
|
||||
# Format tags based on result source
|
||||
if result.get("source") == "openlibrary":
|
||||
if source == "openlibrary":
|
||||
# For OpenLibrary: title, author, year
|
||||
title = result.get("title", "")
|
||||
author = result.get("author", "")
|
||||
year = result.get("year", "")
|
||||
author = ", ".join(metadata.get("authors", [])) if isinstance(metadata.get("authors"), list) else metadata.get("authors", "")
|
||||
year = str(metadata.get("year", ""))
|
||||
tags = []
|
||||
if title:
|
||||
tags.append(title)
|
||||
@@ -419,38 +309,51 @@ class SearchModal(ModalScreen):
|
||||
if year:
|
||||
tags.append(year)
|
||||
tags_text = "\n".join(tags)
|
||||
else: # soulseek
|
||||
elif source == "soulseek":
|
||||
# For Soulseek: artist, album, title, track
|
||||
tags = []
|
||||
if result.get("artist"):
|
||||
tags.append(result["artist"])
|
||||
if result.get("album"):
|
||||
tags.append(result["album"])
|
||||
if result.get("track"):
|
||||
tags.append(f"Track {result['track']}")
|
||||
if result.get("title"):
|
||||
tags.append(result["title"])
|
||||
if metadata.get("artist"):
|
||||
tags.append(metadata["artist"])
|
||||
if metadata.get("album"):
|
||||
tags.append(metadata["album"])
|
||||
if metadata.get("track_num"):
|
||||
tags.append(f"Track {metadata['track_num']}")
|
||||
if title:
|
||||
tags.append(title)
|
||||
tags_text = "\n".join(tags)
|
||||
else:
|
||||
# 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: dict) -> None:
|
||||
async def _download_book(self, result: Any) -> None:
|
||||
"""Download a book from OpenLibrary using unified downloader."""
|
||||
try:
|
||||
from helper.unified_book_downloader import UnifiedBookDownloader
|
||||
from config import load_config
|
||||
|
||||
logger.info(f"[search-modal] Starting download for: {result.get('title')}")
|
||||
# Convert SearchResult to dict if needed
|
||||
if hasattr(result, 'to_dict'):
|
||||
result_dict = result.to_dict()
|
||||
# Ensure raw_data is populated for downloader
|
||||
if 'raw_data' not in result_dict and result.full_metadata:
|
||||
result_dict['raw_data'] = result.full_metadata
|
||||
else:
|
||||
result_dict = result
|
||||
|
||||
logger.info(f"[search-modal] Starting download for: {result_dict.get('title')}")
|
||||
|
||||
config = load_config()
|
||||
downloader = UnifiedBookDownloader(config=config)
|
||||
|
||||
# Get download options for this book
|
||||
options = downloader.get_download_options(result)
|
||||
options = downloader.get_download_options(result_dict)
|
||||
|
||||
if not options['methods']:
|
||||
logger.warning(f"[search-modal] No download methods available for: {result.get('title')}")
|
||||
logger.warning(f"[search-modal] No download methods available for: {result_dict.get('title')}")
|
||||
# Could show a modal dialog here
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user