This commit is contained in:
nose
2025-12-11 12:47:30 -08:00
parent 6b05dc5552
commit 65d12411a2
92 changed files with 17447 additions and 14308 deletions

View File

@@ -1,4 +1,4 @@
"""Modal for displaying files/URLs to access in web mode."""
"""Modal for displaying files/url to access in web mode."""
from textual.screen import ModalScreen
from textual.containers import Container, Vertical, Horizontal
@@ -93,7 +93,7 @@ class AccessModal(ModalScreen):
yield Label("[bold cyan]File:[/bold cyan]", classes="access-label")
# Display as clickable link using HTML link element for web mode
# Rich link markup `[link=URL]` has parsing issues with URLs containing special chars
# Rich link markup `[link=URL]` has parsing issues with url containing special chars
# Instead, use the HTML link markup that Textual-serve renders as <a> tag
# Format: [link=URL "tooltip"]text[/link] - the quotes help with parsing
link_text = f'[link="{self.item_content}"]Open in Browser[/link]'

View File

@@ -233,8 +233,8 @@ class DownloadModal(ModalScreen):
self.screenshot_checkbox.value = False
self.playlist_merge_checkbox.value = False
# Initialize PDF playlist URLs (set by _handle_pdf_playlist)
self.pdf_urls = []
# Initialize PDF playlist url (set by _handle_pdf_playlist)
self.pdf_url = []
self.is_pdf_playlist = False
# Hide playlist by default (show format select)
@@ -288,10 +288,10 @@ class DownloadModal(ModalScreen):
# Launch the background worker with PDF playlist info
self._submit_worker(url, tags, source, download_enabled, playlist_selection, merge_enabled,
is_pdf_playlist=self.is_pdf_playlist, pdf_urls=self.pdf_urls if self.is_pdf_playlist else [])
is_pdf_playlist=self.is_pdf_playlist, pdf_url=self.pdf_url if self.is_pdf_playlist else [])
@work(thread=True)
def _submit_worker(self, url: str, tags: list, source: str, download_enabled: bool, playlist_selection: str = "", merge_enabled: bool = False, is_pdf_playlist: bool = False, pdf_urls: Optional[list] = None) -> None:
def _submit_worker(self, url: str, tags: list, source: str, download_enabled: bool, playlist_selection: str = "", merge_enabled: bool = False, is_pdf_playlist: bool = False, pdf_url: Optional[list] = None) -> None:
"""Background worker to execute the cmdlet pipeline.
Args:
@@ -302,10 +302,10 @@ class DownloadModal(ModalScreen):
playlist_selection: Playlist track selection (e.g., "1-3", "all", "merge")
merge_enabled: Whether to merge playlist files after download
is_pdf_playlist: Whether this is a PDF pseudo-playlist
pdf_urls: List of PDF URLs if is_pdf_playlist is True
pdf_url: List of PDF url if is_pdf_playlist is True
"""
if pdf_urls is None:
pdf_urls = []
if pdf_url is None:
pdf_url = []
# Initialize worker to None so outer exception handler can check it
worker = None
@@ -340,9 +340,9 @@ class DownloadModal(ModalScreen):
worker.log_step("Download initiated")
# Handle PDF playlist specially
if is_pdf_playlist and pdf_urls:
logger.info(f"Processing PDF playlist with {len(pdf_urls)} PDFs")
self._handle_pdf_playlist_download(pdf_urls, tags, playlist_selection, merge_enabled)
if is_pdf_playlist and pdf_url:
logger.info(f"Processing PDF playlist with {len(pdf_url)} PDFs")
self._handle_pdf_playlist_download(pdf_url, tags, playlist_selection, merge_enabled)
self.app.call_from_thread(self._hide_progress)
self.app.call_from_thread(self.dismiss)
return
@@ -690,7 +690,7 @@ class DownloadModal(ModalScreen):
'media_kind': 'audio',
'hash_hex': None,
'hash': None,
'known_urls': [],
'url': [],
'title': filepath_obj.stem
})()
files_to_merge.append(file_result)
@@ -934,8 +934,8 @@ class DownloadModal(ModalScreen):
"""Scrape metadata from URL(s) in URL textarea - wipes tags and source.
This is triggered by Ctrl+T when URL textarea is focused.
Supports single URL or multiple URLs (newline/comma-separated).
For multiple PDF URLs, creates pseudo-playlist for merge workflow.
Supports single URL or multiple url (newline/comma-separated).
For multiple PDF url, creates pseudo-playlist for merge workflow.
"""
try:
text = self.paragraph_textarea.text.strip()
@@ -943,29 +943,29 @@ class DownloadModal(ModalScreen):
logger.warning("No URL to scrape metadata from")
return
# Parse multiple URLs (newline or comma-separated)
urls = []
# Parse multiple url (newline or comma-separated)
url = []
for line in text.split('\n'):
line = line.strip()
if line:
# Handle comma-separated URLs within a line
# Handle comma-separated url within a line
for url in line.split(','):
url = url.strip()
if url:
urls.append(url)
url.append(url)
# Check if multiple URLs provided
if len(urls) > 1:
logger.info(f"Detected {len(urls)} URLs - checking for PDF pseudo-playlist")
# Check if all URLs appear to be PDFs
all_pdfs = all(url.endswith('.pdf') or 'pdf' in url.lower() for url in urls)
# Check if multiple url provided
if len(url) > 1:
logger.info(f"Detected {len(url)} url - checking for PDF pseudo-playlist")
# Check if all url appear to be PDFs
all_pdfs = all(url.endswith('.pdf') or 'pdf' in url.lower() for url in url)
if all_pdfs:
logger.info(f"All URLs are PDFs - creating pseudo-playlist")
self._handle_pdf_playlist(urls)
logger.info(f"All url are PDFs - creating pseudo-playlist")
self._handle_pdf_playlist(url)
return
# Single URL - proceed with normal metadata scraping
url = urls[0] if urls else text.strip()
url = url[0] if url else text.strip()
logger.info(f"Scraping fresh metadata from: {url}")
# Check if tags are already provided in textarea
@@ -1044,21 +1044,21 @@ class DownloadModal(ModalScreen):
)
def _handle_pdf_playlist(self, pdf_urls: list) -> None:
"""Handle multiple PDF URLs as a pseudo-playlist.
def _handle_pdf_playlist(self, pdf_url: list) -> None:
"""Handle multiple PDF url as a pseudo-playlist.
Creates a playlist-like structure with PDF metadata for merge workflow.
Extracts title from URL or uses default naming.
Args:
pdf_urls: List of PDF URLs to process
pdf_url: List of PDF url to process
"""
try:
logger.info(f"Creating PDF pseudo-playlist with {len(pdf_urls)} items")
logger.info(f"Creating PDF pseudo-playlist with {len(pdf_url)} items")
# Create playlist items from PDF URLs
# Create playlist items from PDF url
playlist_items = []
for idx, url in enumerate(pdf_urls, 1):
for idx, url in enumerate(pdf_url, 1):
# Extract filename from URL for display
try:
# Get filename from URL path
@@ -1083,15 +1083,15 @@ class DownloadModal(ModalScreen):
# Build minimal metadata structure for UI population
metadata = {
'title': f'{len(pdf_urls)} PDF Documents',
'title': f'{len(pdf_url)} PDF Documents',
'tags': [],
'formats': [('pdf', 'pdf')], # Default format is PDF
'playlist_items': playlist_items,
'is_pdf_playlist': True # Mark as PDF pseudo-playlist
}
# Store URLs for later use during merge
self.pdf_urls = pdf_urls
# Store url for later use during merge
self.pdf_url = pdf_url
self.is_pdf_playlist = True
# Populate the modal with metadata
@@ -1099,7 +1099,7 @@ class DownloadModal(ModalScreen):
self._populate_from_metadata(metadata, wipe_tags_and_source=True)
self.app.notify(
f"Loaded {len(pdf_urls)} PDFs as playlist",
f"Loaded {len(pdf_url)} PDFs as playlist",
title="PDF Playlist",
severity="information",
timeout=3
@@ -1115,11 +1115,11 @@ class DownloadModal(ModalScreen):
)
def _handle_pdf_playlist_download(self, pdf_urls: list, tags: list, selection: str, merge_enabled: bool) -> None:
def _handle_pdf_playlist_download(self, pdf_url: list, tags: list, selection: str, merge_enabled: bool) -> None:
"""Download and merge PDF playlist.
Args:
pdf_urls: List of PDF URLs to download
pdf_url: List of PDF url to download
tags: Tags to apply to the merged PDF
selection: Selection string like "1-3" or "1,3,5"
merge_enabled: Whether to merge the PDFs
@@ -1141,7 +1141,7 @@ class DownloadModal(ModalScreen):
# Create temporary list of playlist items for selection parsing
# We need this because _parse_playlist_selection uses self.playlist_items
temp_items = []
for url in pdf_urls:
for url in pdf_url:
temp_items.append({'title': url})
self.playlist_items = temp_items
@@ -1149,20 +1149,20 @@ class DownloadModal(ModalScreen):
selected_indices = self._parse_playlist_selection(selection)
if not selected_indices:
# No valid selection, use all
selected_indices = list(range(len(pdf_urls)))
selected_indices = list(range(len(pdf_url)))
selected_urls = [pdf_urls[i] for i in selected_indices]
selected_url = [pdf_url[i] for i in selected_indices]
logger.info(f"Downloading {len(selected_urls)} selected PDFs for merge")
logger.info(f"Downloading {len(selected_url)} selected PDFs for merge")
# Download PDFs to temporary directory
temp_dir = Path.home() / ".downlow_temp_pdfs"
temp_dir.mkdir(exist_ok=True)
downloaded_files = []
for idx, url in enumerate(selected_urls, 1):
for idx, url in enumerate(selected_url, 1):
try:
logger.info(f"Downloading PDF {idx}/{len(selected_urls)}: {url}")
logger.info(f"Downloading PDF {idx}/{len(selected_url)}: {url}")
response = requests.get(url, timeout=30)
response.raise_for_status()
@@ -1619,7 +1619,7 @@ class DownloadModal(ModalScreen):
)
return
else:
success_msg = "download-data completed successfully"
success_msg = "download-data completed successfully"
logger.info(success_msg)
if worker:
worker.append_stdout(f"{success_msg}\n")
@@ -1670,7 +1670,7 @@ class DownloadModal(ModalScreen):
worker.append_stdout(f"{warning_msg}\n")
else:
if worker:
worker.append_stdout("Tags applied successfully\n")
worker.append_stdout("Tags applied successfully\n")
except Exception as e:
error_msg = f"❌ Tagging error: {e}"
logger.error(error_msg, exc_info=True)
@@ -1684,7 +1684,7 @@ class DownloadModal(ModalScreen):
worker.append_stdout(f"{warning_msg}\n")
else:
if worker:
worker.append_stdout("Download complete (no tags to apply)\n")
worker.append_stdout("Download complete (no tags to apply)\n")
def _show_format_select(self) -> None:
"""Show format select (always visible for single files)."""
@@ -1770,9 +1770,9 @@ class DownloadModal(ModalScreen):
# Namespaces to exclude (metadata-only, not user-facing)
excluded_namespaces = {
'hash', # Hash values (internal)
'known_url', # URLs (internal)
'url', # url (internal)
'relationship', # Internal relationships
'url', # URLs (internal)
'url', # url (internal)
}
# Add all other tags

View File

@@ -350,9 +350,9 @@ class ExportModal(ModalScreen):
if tag:
export_tags.add(tag)
# For Hydrus export, filter out metadata-only tags (hash:, known_url:, relationship:)
# For Hydrus export, filter out metadata-only tags (hash:, url:, relationship:)
if export_to == "libraries" and library == "hydrus":
metadata_prefixes = {'hash:', 'known_url:', 'relationship:'}
metadata_prefixes = {'hash:', 'url:', 'relationship:'}
export_tags = {tag for tag in export_tags if not any(tag.lower().startswith(prefix) for prefix in metadata_prefixes)}
logger.info(f"Filtered tags for Hydrus - removed metadata tags, {len(export_tags)} tags remaining")
@@ -404,9 +404,9 @@ class ExportModal(ModalScreen):
metadata = self.result_data.get('metadata', {})
# Extract file source info from result_data (passed by hub-ui)
file_hash = self.result_data.get('file_hash')
file_url = self.result_data.get('file_url')
file_path = self.result_data.get('file_path') # For local files
file_hash = self.result_data.get('hash') or self.result_data.get('file_hash')
file_url = self.result_data.get('url') or self.result_data.get('file_url')
file_path = self.result_data.get('path') or self.result_data.get('file_path') # For local files
source = self.result_data.get('source', 'unknown')
# Prepare export data
@@ -419,8 +419,11 @@ class ExportModal(ModalScreen):
'format': file_format,
'metadata': metadata,
'original_data': self.result_data,
'hash': file_hash,
'file_hash': file_hash,
'url': file_url,
'file_url': file_url,
'path': file_path,
'file_path': file_path, # Pass file path for local files
'source': source,
}

View File

@@ -16,7 +16,7 @@ import asyncio
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
from helper.provider import get_provider
logger = logging.getLogger(__name__)
@@ -183,7 +183,7 @@ class SearchModal(ModalScreen):
else:
# Fallback if no columns defined
row.add_column("Title", res.title)
row.add_column("Target", res.target)
row.add_column("Target", getattr(res, 'path', None) or getattr(res, 'url', None) or getattr(res, 'target', None) or '')
self.current_result_table = table

View File

@@ -197,8 +197,6 @@ class PipelineExecutor:
pipeline_ctx = ctx.PipelineStageContext(stage_index=index, total_stages=total)
ctx.set_stage_context(pipeline_ctx)
ctx.set_active(True)
ctx.set_last_stage(index == total - 1)
try:
return_code = cmd_fn(piped_input, list(stage_args), self._config)
@@ -210,7 +208,6 @@ class PipelineExecutor:
return stage
finally:
ctx.set_stage_context(None)
ctx.set_active(False)
emitted = list(getattr(pipeline_ctx, "emits", []) or [])
stage.emitted = emitted