diff --git a/API/folder.py b/API/folder.py index d6f1bd1..5604fa9 100644 --- a/API/folder.py +++ b/API/folder.py @@ -323,10 +323,16 @@ class API_folder_store: self.connection = sqlite3.connect( str(self.db_path), check_same_thread=False, - timeout=5.0 + timeout=20.0 ) self.connection.row_factory = sqlite3.Row + # Ensure busy_timeout is set immediately for all subsequent ops (including pragmas) + try: + self.connection.execute("PRAGMA busy_timeout = 20000") + except Exception: + pass + # Performance & Size Optimizations # 1. WAL mode for better concurrency and fewer locks self.connection.execute("PRAGMA journal_mode=WAL") @@ -344,10 +350,11 @@ class API_folder_store: self.connection.execute("PRAGMA foreign_keys = ON") # Bound how long sqlite will wait on locks before raising. - try: - self.connection.execute("PRAGMA busy_timeout = 5000") - except Exception: - pass + # Already set to 20000 above, no need to lower it. + # try: + # self.connection.execute("PRAGMA busy_timeout = 5000") + # except Exception: + # pass self._create_tables() @@ -2623,6 +2630,10 @@ class API_folder_store: logger.error(f"Error closing database: {e}", exc_info=True) def __enter__(self): + # Acquire shared lock to serialize access across threads + self._lock_cm = self._with_db_lock() + self._lock_cm.__enter__() + if not self.connection: self._init_db() return self @@ -2631,7 +2642,8 @@ class API_folder_store: try: self.close() finally: - pass + if hasattr(self, "_lock_cm"): + self._lock_cm.__exit__(exc_type, exc_val, exc_tb) # ============================================================================ @@ -3728,7 +3740,7 @@ class LocalLibrarySearchOptimizer: return [] try: - with self.db._db_lock: + with self.db._with_db_lock(): cursor = self.db.connection.cursor() cursor.execute( """ @@ -3760,7 +3772,7 @@ class LocalLibrarySearchOptimizer: return [] try: - with self.db._db_lock: + with self.db._with_db_lock(): cursor = self.db.connection.cursor() cursor.execute( """ diff --git a/SYS/rich_display.py b/SYS/rich_display.py index 9925702..7936fab 100644 --- a/SYS/rich_display.py +++ b/SYS/rich_display.py @@ -270,6 +270,12 @@ def render_item_details_panel(item: Dict[str, Any]) -> None: # Create a specialized view with no results rows (only the metadata panel) # We set no_choice=True to hide the "#" column (not that there are any rows). view = ItemDetailView(item_metadata=metadata).set_no_choice(True) + # Ensure no title leaks in (prevents an empty "No results" table from rendering). + try: + view.title = "" + view.header_lines = [] + except Exception: + pass # We want to print ONLY the elements from ItemDetailView, so we don't use stdout_console().print(view) # as that would include the (empty) results panel. diff --git a/Store/Folder.py b/Store/Folder.py index e93c659..3878338 100644 --- a/Store/Folder.py +++ b/Store/Folder.py @@ -101,8 +101,10 @@ class Folder(Store): cached = Folder._scan_cache.get(location_key) if cached is None: try: + debug(f"[folder] Initializing library scan for {location_path}...") initializer = LocalLibraryInitializer(location_path) stats = initializer.scan_and_index() or {} + debug(f"[folder] Scan complete. Stats: {stats}") files_new = int(stats.get("files_new", 0) or 0) sidecars = int(stats.get("sidecars_imported", 0) or 0) total_db = int(stats.get("files_total_db", 0) or 0) diff --git a/cmdlet/_shared.py b/cmdlet/_shared.py index f7f40e2..c09d9d9 100644 --- a/cmdlet/_shared.py +++ b/cmdlet/_shared.py @@ -2736,6 +2736,12 @@ def register_url_with_local_library( if not storage_path: return False + # Optimization: Don't open DB if file isn't in library root + try: + path_obj.resolve().relative_to(Path(storage_path).resolve()) + except ValueError: + return False + with API_folder_store(storage_path) as db: file_hash = db.get_file_hash(path_obj) if not file_hash: diff --git a/cmdlet/add_file.py b/cmdlet/add_file.py index 457d4f0..8d4a484 100644 --- a/cmdlet/add_file.py +++ b/cmdlet/add_file.py @@ -668,50 +668,72 @@ class Add_File(Cmdlet): # This keeps output consistent and ensures @N selection works for multi-item ingests. if want_final_search_file and collected_payloads: try: - hashes: List[str] = [] - for payload in collected_payloads: - h = payload.get("hash") if isinstance(payload, dict) else None - if isinstance(h, str) and len(h) == 64: - hashes.append(h) - # Deduplicate while preserving order - seen: set[str] = set() - hashes = [h for h in hashes if not (h in seen or seen.add(h))] - - if use_steps and steps_started: - progress.step("refreshing display") - - refreshed_items = Add_File._try_emit_search_file_by_hashes( - store=str(location), - hash_values=hashes, - config=config, - store_instance=storage_registry, - ) - debug(f"[add-file] Internal refresh returned refreshed_items count={len(refreshed_items) if refreshed_items else 0}") - if not refreshed_items: - # Fallback: at least show the add-file payloads as a display overlay + # If this was a single-item ingest, render the detailed item display + # directly from the payload and skip the internal search-file refresh. + if len(collected_payloads) == 1: from SYS.result_table import ResultTable + from SYS.rich_display import render_item_details_panel - # If this was a single-item ingest, render the detailed item display - # directly from the payload to avoid DB refresh contention. - detail_rendered = False - if len(collected_payloads) == 1: + # Stop the live pipeline progress UI before rendering the details panel. + # This prevents the progress display from lingering on screen. + try: + live_progress = ctx.get_live_progress() + except Exception: + live_progress = None + if live_progress is not None: try: - from SYS.rich_display import render_item_details_panel - - render_item_details_panel(collected_payloads[0]) - table = ResultTable("Result") - table.add_result(collected_payloads[0]) - setattr(table, "_rendered_by_cmdlet", True) - ctx.set_last_result_table_overlay( - table, - collected_payloads, - subject=collected_payloads[0] - ) - detail_rendered = True + stage_ctx = ctx.get_stage_context() + pipe_idx = getattr(stage_ctx, "pipe_index", None) + if isinstance(pipe_idx, int): + live_progress.finish_pipe( + int(pipe_idx), + force_complete=True + ) except Exception: - detail_rendered = False + pass + try: + live_progress.stop() + except Exception: + pass + try: + if hasattr(ctx, "set_live_progress"): + ctx.set_live_progress(None) + except Exception: + pass + + render_item_details_panel(collected_payloads[0]) + table = ResultTable("Result") + table.add_result(collected_payloads[0]) + setattr(table, "_rendered_by_cmdlet", True) + ctx.set_last_result_table_overlay( + table, + collected_payloads, + subject=collected_payloads[0] + ) + else: + hashes: List[str] = [] + for payload in collected_payloads: + h = payload.get("hash") if isinstance(payload, dict) else None + if isinstance(h, str) and len(h) == 64: + hashes.append(h) + # Deduplicate while preserving order + seen: set[str] = set() + hashes = [h for h in hashes if not (h in seen or seen.add(h))] + + if use_steps and steps_started: + progress.step("refreshing display") + + refreshed_items = Add_File._try_emit_search_file_by_hashes( + store=str(location), + hash_values=hashes, + config=config, + store_instance=storage_registry, + ) + debug(f"[add-file] Internal refresh returned refreshed_items count={len(refreshed_items) if refreshed_items else 0}") + if not refreshed_items: + # Fallback: at least show the add-file payloads as a display overlay + from SYS.result_table import ResultTable - if not detail_rendered: table = ResultTable("Result") for payload in collected_payloads: table.add_result(payload) @@ -800,6 +822,16 @@ class Add_File(Cmdlet): table = ctx.get_last_result_table() items = ctx.get_last_result_items() if table is not None and items: + # If we have a single item refresh, render it as a panel immediately + # and suppress the table output from the CLI runner. + if len(items) == 1: + try: + from SYS.rich_display import render_item_details_panel + render_item_details_panel(items[0]) + setattr(table, "_rendered_by_cmdlet", True) + except Exception as exc: + debug(f"[add-file] Item details render failed: {exc}") + ctx.set_last_result_table_overlay( table, items, diff --git a/cmdlet/search_file.py b/cmdlet/search_file.py index 6731696..62a9e0b 100644 --- a/cmdlet/search_file.py +++ b/cmdlet/search_file.py @@ -239,7 +239,9 @@ class search_file(Cmdlet): pass db = None - if library_root: + # Disable Folder DB usage for "external" searches when not using a folder store + # db = None + if library_root and False: # Disabled to prevent 'database is locked' errors during external searches try: from API.folder import API_folder_store @@ -936,7 +938,17 @@ class search_file(Cmdlet): pass if refresh_mode: - ctx.set_last_result_table_preserve_history(table, results_list) + # For internal refresh, use overlay mode to avoid adding to history + try: + # Parse out the store/hash context if possible + subject_context = None + if "hash:" in query: + subject_hash = query.split("hash:")[1].split(",")[0].strip() + subject_context = {"store": backend_to_search, "hash": subject_hash} + + ctx.set_last_result_table_overlay(table, results_list, subject=subject_context) + except Exception: + ctx.set_last_result_table_preserve_history(table, results_list) else: ctx.set_last_result_table(table, results_list) db.append_worker_stdout( diff --git a/tool/ytdlp.py b/tool/ytdlp.py index cd62804..22c39a1 100644 --- a/tool/ytdlp.py +++ b/tool/ytdlp.py @@ -574,11 +574,12 @@ class YtDlpTool: fmt = opts.ytdl_format or self.default_format(opts.mode) base_options["format"] = fmt - if opts.mode == "audio": - base_options["postprocessors"] = [{ - "key": "FFmpegExtractAudio" - }] - else: + # if opts.mode == "audio": + # base_options["postprocessors"] = [{ + # "key": "FFmpegExtractAudio" + # }] + + if opts.mode != "audio": format_sort = self.defaults.format_sort or [ "res:4320", "res:2880",