d
This commit is contained in:
@@ -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(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -668,6 +668,49 @@ 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:
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
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:
|
||||||
|
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] = []
|
hashes: List[str] = []
|
||||||
for payload in collected_payloads:
|
for payload in collected_payloads:
|
||||||
h = payload.get("hash") if isinstance(payload, dict) else None
|
h = payload.get("hash") if isinstance(payload, dict) else None
|
||||||
@@ -691,27 +734,6 @@ class Add_File(Cmdlet):
|
|||||||
# Fallback: at least show the add-file payloads as a display overlay
|
# Fallback: at least show the add-file payloads as a display overlay
|
||||||
from SYS.result_table import ResultTable
|
from SYS.result_table import ResultTable
|
||||||
|
|
||||||
# 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:
|
|
||||||
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
|
|
||||||
except Exception:
|
|
||||||
detail_rendered = False
|
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -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,6 +938,16 @@ class search_file(Cmdlet):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if refresh_mode:
|
if refresh_mode:
|
||||||
|
# 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)
|
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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user