This commit is contained in:
2026-01-15 16:26:22 -08:00
parent dabc8f9d51
commit cb679235bd
7 changed files with 125 additions and 54 deletions

View File

@@ -323,10 +323,16 @@ class API_folder_store:
self.connection = sqlite3.connect( self.connection = sqlite3.connect(
str(self.db_path), str(self.db_path),
check_same_thread=False, check_same_thread=False,
timeout=5.0 timeout=20.0
) )
self.connection.row_factory = sqlite3.Row 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 # Performance & Size Optimizations
# 1. WAL mode for better concurrency and fewer locks # 1. WAL mode for better concurrency and fewer locks
self.connection.execute("PRAGMA journal_mode=WAL") self.connection.execute("PRAGMA journal_mode=WAL")
@@ -344,10 +350,11 @@ class API_folder_store:
self.connection.execute("PRAGMA foreign_keys = ON") self.connection.execute("PRAGMA foreign_keys = ON")
# Bound how long sqlite will wait on locks before raising. # Bound how long sqlite will wait on locks before raising.
try: # Already set to 20000 above, no need to lower it.
self.connection.execute("PRAGMA busy_timeout = 5000") # try:
except Exception: # self.connection.execute("PRAGMA busy_timeout = 5000")
pass # except Exception:
# pass
self._create_tables() self._create_tables()
@@ -2623,6 +2630,10 @@ class API_folder_store:
logger.error(f"Error closing database: {e}", exc_info=True) logger.error(f"Error closing database: {e}", exc_info=True)
def __enter__(self): 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: if not self.connection:
self._init_db() self._init_db()
return self return self
@@ -2631,7 +2642,8 @@ class API_folder_store:
try: try:
self.close() self.close()
finally: finally:
pass if hasattr(self, "_lock_cm"):
self._lock_cm.__exit__(exc_type, exc_val, exc_tb)
# ============================================================================ # ============================================================================
@@ -3728,7 +3740,7 @@ class LocalLibrarySearchOptimizer:
return [] return []
try: try:
with self.db._db_lock: with self.db._with_db_lock():
cursor = self.db.connection.cursor() cursor = self.db.connection.cursor()
cursor.execute( cursor.execute(
""" """
@@ -3760,7 +3772,7 @@ class LocalLibrarySearchOptimizer:
return [] return []
try: try:
with self.db._db_lock: with self.db._with_db_lock():
cursor = self.db.connection.cursor() cursor = self.db.connection.cursor()
cursor.execute( cursor.execute(
""" """

View File

@@ -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) # 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). # We set no_choice=True to hide the "#" column (not that there are any rows).
view = ItemDetailView(item_metadata=metadata).set_no_choice(True) 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) # 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. # as that would include the (empty) results panel.

View File

@@ -101,8 +101,10 @@ class Folder(Store):
cached = Folder._scan_cache.get(location_key) cached = Folder._scan_cache.get(location_key)
if cached is None: if cached is None:
try: try:
debug(f"[folder] Initializing library scan for {location_path}...")
initializer = LocalLibraryInitializer(location_path) initializer = LocalLibraryInitializer(location_path)
stats = initializer.scan_and_index() or {} stats = initializer.scan_and_index() or {}
debug(f"[folder] Scan complete. Stats: {stats}")
files_new = int(stats.get("files_new", 0) or 0) files_new = int(stats.get("files_new", 0) or 0)
sidecars = int(stats.get("sidecars_imported", 0) or 0) sidecars = int(stats.get("sidecars_imported", 0) or 0)
total_db = int(stats.get("files_total_db", 0) or 0) total_db = int(stats.get("files_total_db", 0) or 0)

View File

@@ -2736,6 +2736,12 @@ def register_url_with_local_library(
if not storage_path: if not storage_path:
return False 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: with API_folder_store(storage_path) as db:
file_hash = db.get_file_hash(path_obj) file_hash = db.get_file_hash(path_obj)
if not file_hash: if not file_hash:

View File

@@ -668,50 +668,72 @@ class Add_File(Cmdlet):
# This keeps output consistent and ensures @N selection works for multi-item ingests. # This keeps output consistent and ensures @N selection works for multi-item ingests.
if want_final_search_file and collected_payloads: if want_final_search_file and collected_payloads:
try: try:
hashes: List[str] = [] # If this was a single-item ingest, render the detailed item display
for payload in collected_payloads: # directly from the payload and skip the internal search-file refresh.
h = payload.get("hash") if isinstance(payload, dict) else None if len(collected_payloads) == 1:
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 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 # Stop the live pipeline progress UI before rendering the details panel.
# directly from the payload to avoid DB refresh contention. # This prevents the progress display from lingering on screen.
detail_rendered = False try:
if len(collected_payloads) == 1: live_progress = ctx.get_live_progress()
except Exception:
live_progress = None
if live_progress is not None:
try: try:
from SYS.rich_display import render_item_details_panel stage_ctx = ctx.get_stage_context()
pipe_idx = getattr(stage_ctx, "pipe_index", None)
render_item_details_panel(collected_payloads[0]) if isinstance(pipe_idx, int):
table = ResultTable("Result") live_progress.finish_pipe(
table.add_result(collected_payloads[0]) int(pipe_idx),
setattr(table, "_rendered_by_cmdlet", True) force_complete=True
ctx.set_last_result_table_overlay( )
table,
collected_payloads,
subject=collected_payloads[0]
)
detail_rendered = True
except Exception: 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") table = ResultTable("Result")
for payload in collected_payloads: for payload in collected_payloads:
table.add_result(payload) table.add_result(payload)
@@ -800,6 +822,16 @@ class Add_File(Cmdlet):
table = ctx.get_last_result_table() table = ctx.get_last_result_table()
items = ctx.get_last_result_items() items = ctx.get_last_result_items()
if table is not None and 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( ctx.set_last_result_table_overlay(
table, table,
items, items,

View File

@@ -239,7 +239,9 @@ class search_file(Cmdlet):
pass pass
db = None 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: try:
from API.folder import API_folder_store from API.folder import API_folder_store
@@ -936,7 +938,17 @@ class search_file(Cmdlet):
pass pass
if refresh_mode: 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: else:
ctx.set_last_result_table(table, results_list) ctx.set_last_result_table(table, results_list)
db.append_worker_stdout( db.append_worker_stdout(

View File

@@ -574,11 +574,12 @@ class YtDlpTool:
fmt = opts.ytdl_format or self.default_format(opts.mode) fmt = opts.ytdl_format or self.default_format(opts.mode)
base_options["format"] = fmt base_options["format"] = fmt
if opts.mode == "audio": # if opts.mode == "audio":
base_options["postprocessors"] = [{ # base_options["postprocessors"] = [{
"key": "FFmpegExtractAudio" # "key": "FFmpegExtractAudio"
}] # }]
else:
if opts.mode != "audio":
format_sort = self.defaults.format_sort or [ format_sort = self.defaults.format_sort or [
"res:4320", "res:4320",
"res:2880", "res:2880",