Add YAPF style + ignore, and format tracked Python files
This commit is contained in:
@@ -54,15 +54,20 @@ class DownloadModal(ModalScreen):
|
||||
"""Modal screen for initiating new download requests."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
Binding("ctrl+enter", "submit", "Submit"),
|
||||
Binding("escape",
|
||||
"cancel",
|
||||
"Cancel"),
|
||||
Binding("ctrl+enter",
|
||||
"submit",
|
||||
"Submit"),
|
||||
]
|
||||
|
||||
CSS_PATH = "download.tcss"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_submit: Optional[Callable[[dict], None]] = None,
|
||||
on_submit: Optional[Callable[[dict],
|
||||
None]] = None,
|
||||
available_sources: Optional[list] = None,
|
||||
config: Optional[dict] = None,
|
||||
):
|
||||
@@ -221,7 +226,10 @@ class DownloadModal(ModalScreen):
|
||||
self.progress_bar = self.query_one("#progress_bar", ProgressBar)
|
||||
self.playlist_tree = self.query_one("#playlist_tree", Tree)
|
||||
self.playlist_input = self.query_one("#playlist_input", Input)
|
||||
self.playlist_merge_checkbox = self.query_one("#playlist_merge_checkbox", Checkbox)
|
||||
self.playlist_merge_checkbox = self.query_one(
|
||||
"#playlist_merge_checkbox",
|
||||
Checkbox
|
||||
)
|
||||
|
||||
# Set default actions
|
||||
self.download_checkbox.value = True
|
||||
@@ -251,7 +259,11 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
if not url:
|
||||
logger.warning("Download request missing URL")
|
||||
self.app.notify("URL is required", title="Missing Input", severity="warning")
|
||||
self.app.notify(
|
||||
"URL is required",
|
||||
title="Missing Input",
|
||||
severity="warning"
|
||||
)
|
||||
return
|
||||
|
||||
# Parse tags (one per line)
|
||||
@@ -363,7 +375,12 @@ class DownloadModal(ModalScreen):
|
||||
# Handle PDF playlist specially
|
||||
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._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
|
||||
@@ -376,7 +393,10 @@ class DownloadModal(ModalScreen):
|
||||
if not get_cmdlet:
|
||||
logger.error("cmdlet module not available")
|
||||
self.app.call_from_thread(
|
||||
self.app.notify, "cmdlet system unavailable", title="Error", severity="error"
|
||||
self.app.notify,
|
||||
"cmdlet system unavailable",
|
||||
title="Error",
|
||||
severity="error"
|
||||
)
|
||||
self.app.call_from_thread(self._hide_progress)
|
||||
return
|
||||
@@ -402,7 +422,9 @@ class DownloadModal(ModalScreen):
|
||||
# Always use yt-dlp's native --playlist-items for playlists
|
||||
if playlist_selection:
|
||||
# User provided specific selection
|
||||
ytdlp_selection = self._convert_selection_to_ytdlp(playlist_selection)
|
||||
ytdlp_selection = self._convert_selection_to_ytdlp(
|
||||
playlist_selection
|
||||
)
|
||||
logger.info(
|
||||
f"Playlist with user selection: {playlist_selection} → {ytdlp_selection}"
|
||||
)
|
||||
@@ -442,17 +464,22 @@ class DownloadModal(ModalScreen):
|
||||
logger.info(f"Calling download_cmdlet...")
|
||||
cmd_config = (
|
||||
dict(self.config)
|
||||
if isinstance(self.config, dict)
|
||||
else self.config
|
||||
if isinstance(self.config,
|
||||
dict) else self.config
|
||||
)
|
||||
if isinstance(cmd_config, dict):
|
||||
cmd_config["_quiet_background_output"] = True
|
||||
returncode = download_cmdlet(result_obj, cmdlet_args, cmd_config)
|
||||
returncode = download_cmdlet(
|
||||
result_obj,
|
||||
cmdlet_args,
|
||||
cmd_config
|
||||
)
|
||||
logger.info(f"download_cmdlet returned: {returncode}")
|
||||
except Exception as cmdlet_error:
|
||||
# If cmdlet throws an exception, log it
|
||||
logger.error(
|
||||
f"❌ download-cmdlet exception: {cmdlet_error}", exc_info=True
|
||||
f"❌ download-cmdlet exception: {cmdlet_error}",
|
||||
exc_info=True
|
||||
)
|
||||
if worker:
|
||||
import traceback
|
||||
@@ -488,16 +515,21 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
# Log the output so it gets captured by WorkerLoggingHandler
|
||||
if stdout_text:
|
||||
logger.info(f"[{download_cmdlet_name} output]\n{stdout_text}")
|
||||
logger.info(
|
||||
f"[{download_cmdlet_name} output]\n{stdout_text}"
|
||||
)
|
||||
if stderr_text:
|
||||
logger.info(f"[{download_cmdlet_name} stderr]\n{stderr_text}")
|
||||
logger.info(
|
||||
f"[{download_cmdlet_name} stderr]\n{stderr_text}"
|
||||
)
|
||||
if returncode != 0:
|
||||
download_failed_msg = f"❌ {download_cmdlet_name} stage failed with code {returncode}\nstdout: {stdout_text}\nstderr: {stderr_text}"
|
||||
logger.error(download_failed_msg)
|
||||
if worker:
|
||||
worker.append_stdout(f"\n{download_failed_msg}\n")
|
||||
worker.finish(
|
||||
"error", "Download stage failed - see logs above for details"
|
||||
"error",
|
||||
"Download stage failed - see logs above for details"
|
||||
)
|
||||
|
||||
# Log to stderr as well so it shows in terminal
|
||||
@@ -528,26 +560,23 @@ class DownloadModal(ModalScreen):
|
||||
import re
|
||||
|
||||
http_match = re.search(
|
||||
r"HTTP Error (\d{3})", stderr_text + stdout_text, re.IGNORECASE
|
||||
r"HTTP Error (\d{3})",
|
||||
stderr_text + stdout_text,
|
||||
re.IGNORECASE
|
||||
)
|
||||
if http_match:
|
||||
error_reason = f"HTTP Error {http_match.group(1)}: Server returned an error"
|
||||
else:
|
||||
error_reason = "HTTP error from server"
|
||||
elif (
|
||||
"no such file or directory" in error_text
|
||||
or "file not found" in error_text
|
||||
):
|
||||
elif ("no such file or directory" in error_text
|
||||
or "file not found" in error_text):
|
||||
error_reason = (
|
||||
"File not found (yt-dlp may not be installed or not in PATH)"
|
||||
)
|
||||
elif "unable to download" in error_text:
|
||||
error_reason = "Unable to download video (network issue or content unavailable)"
|
||||
elif (
|
||||
"connection" in error_text
|
||||
or "timeout" in error_text
|
||||
or "timed out" in error_text
|
||||
):
|
||||
elif ("connection" in error_text or "timeout" in error_text
|
||||
or "timed out" in error_text):
|
||||
error_reason = "Network connection failed or timed out"
|
||||
elif "permission" in error_text or "access denied" in error_text:
|
||||
error_reason = (
|
||||
@@ -567,22 +596,20 @@ class DownloadModal(ModalScreen):
|
||||
# If still unknown, try to extract last line of stderr as it often contains the actual error
|
||||
if error_reason == "Unknown error":
|
||||
stderr_lines = [
|
||||
line.strip() for line in stderr_text.split("\n") if line.strip()
|
||||
line.strip() for line in stderr_text.split("\n")
|
||||
if line.strip()
|
||||
]
|
||||
if stderr_lines:
|
||||
# Look for error-like lines (usually contain "error", "failed", "ERROR", etc)
|
||||
for line in reversed(stderr_lines):
|
||||
if any(
|
||||
keyword in line.lower()
|
||||
for keyword in [
|
||||
if any(keyword in line.lower() for keyword in [
|
||||
"error",
|
||||
"failed",
|
||||
"exception",
|
||||
"traceback",
|
||||
"warning",
|
||||
]
|
||||
):
|
||||
error_reason = line[:150] # Limit to 150 chars
|
||||
"warning", ]):
|
||||
error_reason = line[:150
|
||||
] # Limit to 150 chars
|
||||
break
|
||||
# If no error keyword found, use the last line
|
||||
if error_reason == "Unknown error":
|
||||
@@ -613,9 +640,13 @@ class DownloadModal(ModalScreen):
|
||||
worker.append_stdout(f"\n❌ DOWNLOAD FAILED\n")
|
||||
worker.append_stdout(f"Reason: {error_reason}\n")
|
||||
if stderr_text and stderr_text.strip():
|
||||
worker.append_stdout(f"\nFull error output:\n{stderr_text}\n")
|
||||
worker.append_stdout(
|
||||
f"\nFull error output:\n{stderr_text}\n"
|
||||
)
|
||||
if stdout_text and stdout_text.strip():
|
||||
worker.append_stdout(f"\nStandard output:\n{stdout_text}\n")
|
||||
worker.append_stdout(
|
||||
f"\nStandard output:\n{stdout_text}\n"
|
||||
)
|
||||
# Don't try to tag if download failed
|
||||
self.app.call_from_thread(self._hide_progress)
|
||||
self.app.call_from_thread(self.dismiss)
|
||||
@@ -623,11 +654,17 @@ class DownloadModal(ModalScreen):
|
||||
else:
|
||||
download_succeeded = True
|
||||
# Always log output at INFO level so we can see what happened
|
||||
logger.info(f"{download_cmdlet_name} stage completed successfully")
|
||||
logger.info(
|
||||
f"{download_cmdlet_name} stage completed successfully"
|
||||
)
|
||||
if stdout_text:
|
||||
logger.info(f"{download_cmdlet_name} stdout:\n{stdout_text}")
|
||||
logger.info(
|
||||
f"{download_cmdlet_name} stdout:\n{stdout_text}"
|
||||
)
|
||||
if stderr_text:
|
||||
logger.info(f"{download_cmdlet_name} stderr:\n{stderr_text}")
|
||||
logger.info(
|
||||
f"{download_cmdlet_name} stderr:\n{stderr_text}"
|
||||
)
|
||||
|
||||
# Log step to worker
|
||||
if worker:
|
||||
@@ -641,7 +678,7 @@ class DownloadModal(ModalScreen):
|
||||
if self.is_playlist and merge_enabled:
|
||||
# Get output directory
|
||||
from pathlib import Path
|
||||
from config import resolve_output_dir
|
||||
from SYS.config import resolve_output_dir
|
||||
|
||||
output_dir = resolve_output_dir(self.config)
|
||||
logger.info(
|
||||
@@ -660,7 +697,9 @@ class DownloadModal(ModalScreen):
|
||||
if filename:
|
||||
full_path = output_dir / filename
|
||||
if full_path.exists():
|
||||
extracted_files.append(str(full_path))
|
||||
extracted_files.append(
|
||||
str(full_path)
|
||||
)
|
||||
logger.debug(
|
||||
f"Found downloaded file from output: {filename}"
|
||||
)
|
||||
@@ -677,24 +716,29 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
current_time = time.time()
|
||||
recent_files = []
|
||||
for f in (
|
||||
list(output_dir.glob("*.mp3"))
|
||||
+ list(output_dir.glob("*.m4a"))
|
||||
+ list(output_dir.glob("*.mp4"))
|
||||
):
|
||||
for f in (list(output_dir.glob("*.mp3")) +
|
||||
list(output_dir.glob("*.m4a")) +
|
||||
list(output_dir.glob("*.mp4"))):
|
||||
# Files modified in last 30 minutes (extended window)
|
||||
if current_time - f.stat().st_mtime < 1800:
|
||||
recent_files.append((f, f.stat().st_mtime))
|
||||
recent_files.append(
|
||||
(f,
|
||||
f.stat().st_mtime)
|
||||
)
|
||||
|
||||
# Sort by modification time to preserve order
|
||||
recent_files.sort(key=lambda x: x[1])
|
||||
downloaded_files = [str(f[0]) for f in recent_files]
|
||||
downloaded_files = [
|
||||
str(f[0]) for f in recent_files
|
||||
]
|
||||
logger.info(
|
||||
f"Found {len(downloaded_files)} recently modified files in directory (fallback)"
|
||||
)
|
||||
|
||||
if downloaded_files:
|
||||
logger.info(f"Found {len(downloaded_files)} files to merge")
|
||||
logger.info(
|
||||
f"Found {len(downloaded_files)} files to merge"
|
||||
)
|
||||
if downloaded_files:
|
||||
logger.info(
|
||||
f"Files to merge: {downloaded_files[:3]}... (showing first 3)"
|
||||
@@ -707,10 +751,12 @@ class DownloadModal(ModalScreen):
|
||||
# Extract path after "Saved to "
|
||||
saved_idx = line.find("Saved to")
|
||||
if saved_idx != -1:
|
||||
path = line[saved_idx + 8 :].strip()
|
||||
path = line[saved_idx + 8:].strip()
|
||||
if path:
|
||||
downloaded_files.append(path)
|
||||
logger.debug(f"Found downloaded file: {path}")
|
||||
logger.debug(
|
||||
f"Found downloaded file: {path}"
|
||||
)
|
||||
|
||||
# For merge scenarios, DON'T set to first file yet - merge first, then tag
|
||||
# For non-merge, set to first file for tagging
|
||||
@@ -733,9 +779,14 @@ class DownloadModal(ModalScreen):
|
||||
+ download_stderr_text
|
||||
)
|
||||
|
||||
logger.info(f"{download_cmdlet_name} stage completed successfully")
|
||||
logger.info(
|
||||
f"{download_cmdlet_name} stage completed successfully"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"{download_cmdlet_name} execution error: {e}", exc_info=True)
|
||||
logger.error(
|
||||
f"{download_cmdlet_name} execution error: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
self.app.call_from_thread(
|
||||
self.app.notify,
|
||||
f"Download error: {e}",
|
||||
@@ -785,9 +836,13 @@ class DownloadModal(ModalScreen):
|
||||
# Extract file list from marker
|
||||
files_line = download_stderr_text.split("\n")[0]
|
||||
if files_line.startswith("DOWNLOADED_FILES:"):
|
||||
files_str = files_line[len("DOWNLOADED_FILES:") :]
|
||||
file_list = [f.strip() for f in files_str.split(",") if f.strip()]
|
||||
logger.info(f"Found {len(file_list)} downloaded files from marker")
|
||||
files_str = files_line[len("DOWNLOADED_FILES:"):]
|
||||
file_list = [
|
||||
f.strip() for f in files_str.split(",") if f.strip()
|
||||
]
|
||||
logger.info(
|
||||
f"Found {len(file_list)} downloaded files from marker"
|
||||
)
|
||||
|
||||
# Create result objects with proper attributes
|
||||
for filepath in file_list:
|
||||
@@ -821,7 +876,9 @@ class DownloadModal(ModalScreen):
|
||||
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
||||
# Pass the list of file results to merge-file
|
||||
merge_returncode = merge_cmdlet(
|
||||
files_to_merge, merge_args, self.config
|
||||
files_to_merge,
|
||||
merge_args,
|
||||
self.config
|
||||
)
|
||||
|
||||
merge_stdout = stdout_buf.getvalue()
|
||||
@@ -863,7 +920,8 @@ class DownloadModal(ModalScreen):
|
||||
# Extract path after "into: "
|
||||
into_idx = line.find("into:")
|
||||
if into_idx != -1:
|
||||
merged_file_path = line[into_idx + 5 :].strip()
|
||||
merged_file_path = line[into_idx +
|
||||
5:].strip()
|
||||
if merged_file_path:
|
||||
logger.info(
|
||||
f"Detected merged file path: {merged_file_path}"
|
||||
@@ -873,16 +931,13 @@ class DownloadModal(ModalScreen):
|
||||
# If not found in stderr, try stdout
|
||||
if not merged_file_path:
|
||||
for line in merge_stdout.split("\n"):
|
||||
if (
|
||||
"merged" in line.lower()
|
||||
or line.endswith(".mp3")
|
||||
or line.endswith(".m4a")
|
||||
):
|
||||
if ("merged" in line.lower()
|
||||
or line.endswith(".mp3")
|
||||
or line.endswith(".m4a")):
|
||||
merged_file_path = line.strip()
|
||||
if (
|
||||
merged_file_path
|
||||
and not merged_file_path.startswith("[")
|
||||
):
|
||||
if (merged_file_path and
|
||||
not merged_file_path.startswith("[")
|
||||
):
|
||||
logger.info(
|
||||
f"Detected merged file path: {merged_file_path}"
|
||||
)
|
||||
@@ -924,11 +979,15 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
# Log step to worker
|
||||
if worker:
|
||||
worker.log_step(f"Starting add-tags stage with {len(tags)} tags...")
|
||||
worker.log_step(
|
||||
f"Starting add-tags stage with {len(tags)} tags..."
|
||||
)
|
||||
|
||||
# Build add-tags arguments. add-tags requires a store; for downloads, default to local sidecar tagging.
|
||||
tag_args = (
|
||||
["-store", "local"] + [str(t) for t in tags] + ["--source", str(source)]
|
||||
["-store",
|
||||
"local"] + [str(t) for t in tags] + ["--source",
|
||||
str(source)]
|
||||
)
|
||||
logger.info(f" Tag args: {tag_args}")
|
||||
logger.info(
|
||||
@@ -944,7 +1003,11 @@ class DownloadModal(ModalScreen):
|
||||
stderr_buf = io.StringIO()
|
||||
|
||||
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
||||
returncode = add_tags_cmdlet(result_obj, tag_args, self.config)
|
||||
returncode = add_tags_cmdlet(
|
||||
result_obj,
|
||||
tag_args,
|
||||
self.config
|
||||
)
|
||||
|
||||
stdout_text = stdout_buf.getvalue()
|
||||
stderr_text = stderr_buf.getvalue()
|
||||
@@ -956,7 +1019,9 @@ class DownloadModal(ModalScreen):
|
||||
logger.info(f"[add-tags stderr]\n{stderr_text}")
|
||||
|
||||
if returncode != 0:
|
||||
logger.error(f"add-tags stage failed with code {returncode}")
|
||||
logger.error(
|
||||
f"add-tags stage failed with code {returncode}"
|
||||
)
|
||||
logger.error(f" stdout: {stdout_text}")
|
||||
logger.error(f" stderr: {stderr_text}")
|
||||
self.app.call_from_thread(
|
||||
@@ -996,7 +1061,10 @@ class DownloadModal(ModalScreen):
|
||||
logger.info(skip_msg)
|
||||
if worker:
|
||||
worker.append_stdout(f"\n{skip_msg}\n")
|
||||
worker.finish("error", "Download stage failed - see logs above for details")
|
||||
worker.finish(
|
||||
"error",
|
||||
"Download stage failed - see logs above for details"
|
||||
)
|
||||
elif tags:
|
||||
logger.info("No tags to add (tags list is empty)")
|
||||
|
||||
@@ -1029,13 +1097,17 @@ class DownloadModal(ModalScreen):
|
||||
pass
|
||||
self.app.call_from_thread(self._hide_progress)
|
||||
self.app.call_from_thread(
|
||||
self.app.notify, f"Error: {e}", title="Error", severity="error"
|
||||
self.app.notify,
|
||||
f"Error: {e}",
|
||||
title="Error",
|
||||
severity="error"
|
||||
)
|
||||
|
||||
def _create_url_result(self, url: str):
|
||||
"""Create a result object from a URL for cmdlet processing."""
|
||||
|
||||
class URLDownloadResult:
|
||||
|
||||
def __init__(self, u):
|
||||
self.target = u
|
||||
self.url = u
|
||||
@@ -1089,9 +1161,13 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
# Check if multiple url provided
|
||||
if len(url) > 1:
|
||||
logger.info(f"Detected {len(url)} url - checking for PDF pseudo-playlist")
|
||||
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)
|
||||
all_pdfs = all(
|
||||
url.endswith(".pdf") or "pdf" in url.lower() for url in url
|
||||
)
|
||||
if all_pdfs:
|
||||
logger.info(f"All url are PDFs - creating pseudo-playlist")
|
||||
self._handle_pdf_playlist(url)
|
||||
@@ -1107,7 +1183,9 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
# Run in background to prevent UI freezing
|
||||
self._scrape_metadata_worker(
|
||||
url, wipe_tags_and_source=wipe_tags, skip_tag_scraping=not wipe_tags
|
||||
url,
|
||||
wipe_tags_and_source=wipe_tags,
|
||||
skip_tag_scraping=not wipe_tags
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -1129,7 +1207,10 @@ class DownloadModal(ModalScreen):
|
||||
if not expand_tag_lists or not process_tags_from_string:
|
||||
logger.warning("tag_helpers not available")
|
||||
self.app.notify(
|
||||
"Tag processing unavailable", title="Error", severity="error", timeout=2
|
||||
"Tag processing unavailable",
|
||||
title="Error",
|
||||
severity="error",
|
||||
timeout=2
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1169,7 +1250,11 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in _action_scrape_tags: {e}", exc_info=True)
|
||||
self.app.notify(f"Error processing tags: {e}", title="Error", severity="error")
|
||||
self.app.notify(
|
||||
f"Error processing tags: {e}",
|
||||
title="Error",
|
||||
severity="error"
|
||||
)
|
||||
|
||||
def _handle_pdf_playlist(self, pdf_url: list) -> None:
|
||||
"""Handle multiple PDF url as a pseudo-playlist.
|
||||
@@ -1196,7 +1281,10 @@ class DownloadModal(ModalScreen):
|
||||
if not filename or filename.endswith(".pdf"):
|
||||
filename = filename or f"pdf_{idx}.pdf"
|
||||
# Remove .pdf extension for display
|
||||
title = filename.replace(".pdf", "").replace("_", " ").replace("-", " ")
|
||||
title = filename.replace(".pdf",
|
||||
"").replace("_",
|
||||
" ").replace("-",
|
||||
" ")
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not extract filename: {e}")
|
||||
title = f"PDF {idx}"
|
||||
@@ -1236,11 +1324,18 @@ class DownloadModal(ModalScreen):
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling PDF playlist: {e}", exc_info=True)
|
||||
self.app.notify(
|
||||
f"Error loading PDF playlist: {e}", title="Error", severity="error", timeout=3
|
||||
f"Error loading PDF playlist: {e}",
|
||||
title="Error",
|
||||
severity="error",
|
||||
timeout=3
|
||||
)
|
||||
|
||||
def _handle_pdf_playlist_download(
|
||||
self, pdf_url: list, tags: list, selection: str, merge_enabled: bool
|
||||
self,
|
||||
pdf_url: list,
|
||||
tags: list,
|
||||
selection: str,
|
||||
merge_enabled: bool
|
||||
) -> None:
|
||||
"""Download and merge PDF playlist.
|
||||
|
||||
@@ -1263,13 +1358,15 @@ class DownloadModal(ModalScreen):
|
||||
try:
|
||||
from pathlib import Path
|
||||
import requests
|
||||
from config import resolve_output_dir
|
||||
from SYS.config import resolve_output_dir
|
||||
|
||||
# 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_url:
|
||||
temp_items.append({"title": url})
|
||||
temp_items.append({
|
||||
"title": url
|
||||
})
|
||||
self.playlist_items = temp_items
|
||||
|
||||
# Parse selection to get which PDFs to download
|
||||
@@ -1339,7 +1436,9 @@ class DownloadModal(ModalScreen):
|
||||
reader = PdfReader(pdf_file)
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
logger.info(f"Added {len(reader.pages)} pages from {pdf_file.name}")
|
||||
logger.info(
|
||||
f"Added {len(reader.pages)} pages from {pdf_file.name}"
|
||||
)
|
||||
|
||||
# Save merged PDF to output directory
|
||||
output_dir = Path(resolve_output_dir(self.config))
|
||||
@@ -1365,6 +1464,7 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
# Create a result object for the PDF
|
||||
class PDFResult:
|
||||
|
||||
def __init__(self, p):
|
||||
self.path = str(p)
|
||||
self.target = str(p)
|
||||
@@ -1380,10 +1480,16 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
tag_args = ["-store", "local"] + [str(t) for t in tags]
|
||||
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
||||
tag_returncode = tag_cmdlet(result_obj, tag_args, self.config)
|
||||
tag_returncode = tag_cmdlet(
|
||||
result_obj,
|
||||
tag_args,
|
||||
self.config
|
||||
)
|
||||
|
||||
if tag_returncode != 0:
|
||||
logger.warning(f"Tag stage returned code {tag_returncode}")
|
||||
logger.warning(
|
||||
f"Tag stage returned code {tag_returncode}"
|
||||
)
|
||||
|
||||
self.app.call_from_thread(
|
||||
self.app.notify,
|
||||
@@ -1440,7 +1546,10 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
@work(thread=True)
|
||||
def _scrape_metadata_worker(
|
||||
self, url: str, wipe_tags_and_source: bool = False, skip_tag_scraping: bool = False
|
||||
self,
|
||||
url: str,
|
||||
wipe_tags_and_source: bool = False,
|
||||
skip_tag_scraping: bool = False
|
||||
) -> None:
|
||||
"""Background worker to scrape metadata using get-tag cmdlet.
|
||||
|
||||
@@ -1456,7 +1565,10 @@ class DownloadModal(ModalScreen):
|
||||
if not get_cmdlet:
|
||||
logger.error("cmdlet module not available")
|
||||
self.app.call_from_thread(
|
||||
self.app.notify, "cmdlet module not available", title="Error", severity="error"
|
||||
self.app.notify,
|
||||
"cmdlet module not available",
|
||||
title="Error",
|
||||
severity="error"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1465,12 +1577,16 @@ class DownloadModal(ModalScreen):
|
||||
if not get_tag_cmdlet:
|
||||
logger.error("get-tag cmdlet not found")
|
||||
self.app.call_from_thread(
|
||||
self.app.notify, "get-tag cmdlet not found", title="Error", severity="error"
|
||||
self.app.notify,
|
||||
"get-tag cmdlet not found",
|
||||
title="Error",
|
||||
severity="error"
|
||||
)
|
||||
return
|
||||
|
||||
# Create a simple result object for the cmdlet
|
||||
class URLResult:
|
||||
|
||||
def __init__(self, u):
|
||||
self.target = u
|
||||
self.hash_hex = None
|
||||
@@ -1489,7 +1605,9 @@ class DownloadModal(ModalScreen):
|
||||
args = [] if skip_tag_scraping else ["-scrape", url]
|
||||
|
||||
with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
|
||||
returncode = get_tag_cmdlet(result_obj, args, {})
|
||||
returncode = get_tag_cmdlet(result_obj,
|
||||
args,
|
||||
{})
|
||||
|
||||
if returncode != 0:
|
||||
error_msg = error_buffer.getvalue()
|
||||
@@ -1559,7 +1677,8 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
# Build metadata dict in the format expected by _populate_from_metadata
|
||||
# If skipping tag scraping, preserve existing tags
|
||||
existing_tags = self.tags_textarea.text.strip().split("\n") if skip_tag_scraping else []
|
||||
existing_tags = self.tags_textarea.text.strip(
|
||||
).split("\n") if skip_tag_scraping else []
|
||||
existing_tags = [tag.strip() for tag in existing_tags if tag.strip()]
|
||||
|
||||
# Extract playlist items if present
|
||||
@@ -1579,7 +1698,11 @@ class DownloadModal(ModalScreen):
|
||||
)
|
||||
|
||||
# Update UI on main thread
|
||||
self.app.call_from_thread(self._populate_from_metadata, metadata, wipe_tags_and_source)
|
||||
self.app.call_from_thread(
|
||||
self._populate_from_metadata,
|
||||
metadata,
|
||||
wipe_tags_and_source
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Metadata worker error: {e}", exc_info=True)
|
||||
@@ -1611,7 +1734,9 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
# Handle keywords (all, merge, a, m) - can be space or comma separated
|
||||
# "ALL MERGE", "A M", "ALL,MERGE" etc all mean download all items
|
||||
if any(kw in selection_str.replace(",", " ").split() for kw in {"A", "ALL", "M", "MERGE"}):
|
||||
if any(kw in selection_str.replace(",",
|
||||
" ").split()
|
||||
for kw in {"A", "ALL", "M", "MERGE"}):
|
||||
# User said to get all items (merge is same as all in this context)
|
||||
return f"1-{max_idx}"
|
||||
|
||||
@@ -1647,7 +1772,9 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
# Handle keywords (all, merge, a, m) - can be space or comma separated
|
||||
# "ALL MERGE", "A M", "ALL,MERGE" etc all mean download all items
|
||||
if any(kw in selection_str.replace(",", " ").split() for kw in {"A", "ALL", "M", "MERGE"}):
|
||||
if any(kw in selection_str.replace(",",
|
||||
" ").split()
|
||||
for kw in {"A", "ALL", "M", "MERGE"}):
|
||||
# User said to get all items
|
||||
return list(range(max_idx))
|
||||
|
||||
@@ -1676,7 +1803,12 @@ class DownloadModal(ModalScreen):
|
||||
return []
|
||||
|
||||
def _execute_download_pipeline(
|
||||
self, result_obj: Any, tags: list, source: str, download_enabled: bool, worker=None
|
||||
self,
|
||||
result_obj: Any,
|
||||
tags: list,
|
||||
source: str,
|
||||
download_enabled: bool,
|
||||
worker=None
|
||||
) -> None:
|
||||
"""Execute the download pipeline for a single item.
|
||||
|
||||
@@ -1694,7 +1826,10 @@ class DownloadModal(ModalScreen):
|
||||
if worker:
|
||||
worker.append_stdout(f"❌ ERROR: {error_msg}\n")
|
||||
self.app.call_from_thread(
|
||||
self.app.notify, "cmdlet system unavailable", title="Error", severity="error"
|
||||
self.app.notify,
|
||||
"cmdlet system unavailable",
|
||||
title="Error",
|
||||
severity="error"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1716,7 +1851,8 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
||||
cmd_config = (
|
||||
dict(self.config) if isinstance(self.config, dict) else self.config
|
||||
dict(self.config) if isinstance(self.config,
|
||||
dict) else self.config
|
||||
)
|
||||
if isinstance(cmd_config, dict):
|
||||
cmd_config["_quiet_background_output"] = True
|
||||
@@ -1760,7 +1896,10 @@ class DownloadModal(ModalScreen):
|
||||
f"{error_msg}\nTraceback:\n{__import__('traceback').format_exc()}\n"
|
||||
)
|
||||
self.app.call_from_thread(
|
||||
self.app.notify, str(e)[:100], title="Download Error", severity="error"
|
||||
self.app.notify,
|
||||
str(e)[:100],
|
||||
title="Download Error",
|
||||
severity="error"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1781,7 +1920,9 @@ class DownloadModal(ModalScreen):
|
||||
stderr_buf = io.StringIO()
|
||||
|
||||
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
||||
tag_returncode = tag_cmdlet(result_obj, tag_args, {})
|
||||
tag_returncode = tag_cmdlet(result_obj,
|
||||
tag_args,
|
||||
{})
|
||||
|
||||
stdout_text = stdout_buf.getvalue()
|
||||
stderr_text = stderr_buf.getvalue()
|
||||
@@ -1860,7 +2001,11 @@ class DownloadModal(ModalScreen):
|
||||
except Exception as e:
|
||||
logger.error(f"Error populating playlist tree: {e}")
|
||||
|
||||
def _populate_from_metadata(self, metadata: dict, wipe_tags_and_source: bool = False) -> None:
|
||||
def _populate_from_metadata(
|
||||
self,
|
||||
metadata: dict,
|
||||
wipe_tags_and_source: bool = False
|
||||
) -> None:
|
||||
"""Populate modal fields from extracted metadata.
|
||||
|
||||
Args:
|
||||
@@ -1965,7 +2110,11 @@ class DownloadModal(ModalScreen):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error populating metadata: {e}", exc_info=True)
|
||||
self.app.notify(f"Failed to populate metadata: {e}", title="Error", severity="error")
|
||||
self.app.notify(
|
||||
f"Failed to populate metadata: {e}",
|
||||
title="Error",
|
||||
severity="error"
|
||||
)
|
||||
|
||||
def on_select_changed(self, event: Select.Changed) -> None:
|
||||
"""Handle Select widget changes (format selection)."""
|
||||
|
||||
Reference in New Issue
Block a user