This commit is contained in:
nose
2025-11-27 18:35:06 -08:00
parent 9eff65d1af
commit ed417c8200
6 changed files with 143 additions and 93 deletions

View File

@@ -330,9 +330,12 @@ class LocalStorageBackend(StorageBackend):
except Exception:
word_regex = None
else:
# Use word boundary for exact terms (backwards compatibility)
# Use custom boundary that treats underscores as separators
# \b treats _ as a word character, so "foo_bar" wouldn't match "bar" with \b
try:
word_regex = re.compile(r'\b' + re.escape(term) + r'\b', re.IGNORECASE)
# Match if not preceded or followed by alphanumeric chars
pattern = r'(?<![a-zA-Z0-9])' + re.escape(term) + r'(?![a-zA-Z0-9])'
word_regex = re.compile(pattern, re.IGNORECASE)
except Exception:
word_regex = None
@@ -459,71 +462,19 @@ class LocalStorageBackend(StorageBackend):
if results:
debug(f"Returning {len(results)} results from DB")
return results
else:
debug("No results found in DB, falling back to filesystem scan")
debug("No results found in DB")
return results
except Exception as e:
log(f"⚠️ Database search failed: {e}", file=sys.stderr)
debug(f"DB search exception details: {e}")
# Fallback to filesystem search if database search fails or returns nothing
debug("Starting filesystem scan...")
recursive = kwargs.get("recursive", True)
pattern = "**/*" if recursive else "*"
# Split query into terms for AND logic
terms = [t.strip() for t in query_lower.replace(',', ' ').split() if t.strip()]
if not terms:
terms = [query_lower]
count = 0
for file_path in search_dir.glob(pattern):
if not file_path.is_file():
continue
lower_name = file_path.name.lower()
if lower_name.endswith('.tags') or lower_name.endswith('.metadata') \
or lower_name.endswith('.notes') or lower_name.endswith('.tags.txt'):
continue
if not match_all:
# Check if ALL terms are present in the filename
# For single terms with wildcards, use fnmatch; otherwise use substring matching
if len(terms) == 1 and ('*' in terms[0] or '?' in terms[0]):
# Wildcard pattern matching for single term
from fnmatch import fnmatch
if not fnmatch(lower_name, terms[0]):
continue
else:
# Substring matching for all terms (AND logic)
if not all(term in lower_name for term in terms):
continue
size_bytes = file_path.stat().st_size
path_str = str(file_path)
results.append({
"name": file_path.stem,
"title": file_path.stem,
"ext": file_path.suffix.lstrip('.'),
"path": path_str,
"target": path_str,
"origin": "local",
"size": size_bytes,
"size_bytes": size_bytes,
})
count += 1
if limit is not None and len(results) >= limit:
break
debug(f"Filesystem scan found {count} matches")
return []
except Exception as exc:
log(f"❌ Local search failed: {exc}", file=sys.stderr)
raise
return results
class HydrusStorageBackend(StorageBackend):
"""File storage backend for Hydrus client."""

View File

@@ -163,13 +163,25 @@ class LocalLibraryDB:
# Use check_same_thread=False to allow multi-threaded access
# This is safe because we're not sharing connections across threads;
# each thread will get its own cursor
self.connection = sqlite3.connect(str(self.db_path), check_same_thread=False)
# Set a generous timeout to avoid "database is locked" errors during heavy concurrency
self.connection = sqlite3.connect(str(self.db_path), check_same_thread=False, timeout=60.0)
self.connection.row_factory = sqlite3.Row
# Enable Write-Ahead Logging (WAL) for better concurrency
self.connection.execute("PRAGMA journal_mode=WAL")
# Enable foreign keys
self.connection.execute("PRAGMA foreign_keys = ON")
self._create_tables()
logger.info(f"Database initialized at {self.db_path}")
except Exception as e:
logger.error(f"Failed to initialize database: {e}", exc_info=True)
if self.connection:
try:
self.connection.close()
except Exception:
pass
self.connection = None
raise
def _create_tables(self) -> None:
@@ -1492,6 +1504,19 @@ class LocalLibrarySearchOptimizer:
except Exception as e:
logger.error(f"Failed to get playlist ID {playlist_id}: {e}")
return None
def delete_playlist(self, playlist_id: int) -> bool:
"""Delete a playlist by ID."""
if not self.db:
return False
try:
cursor = self.db.connection.cursor()
cursor.execute("DELETE FROM playlists WHERE id = ?", (playlist_id,))
self.db.connection.commit()
return cursor.rowcount > 0
except Exception as e:
logger.error(f"Failed to delete playlist ID {playlist_id}: {e}")
return False
if not self.db:
return []
return self.db.search_by_tag(tag, limit)