diff --git a/API/folder.py b/API/folder.py index d943057..9dc32f8 100644 --- a/API/folder.py +++ b/API/folder.py @@ -864,22 +864,23 @@ class API_folder_store: def get_metadata(self, file_hash: str) -> Optional[Dict[str, Any]]: """Get metadata for a file by hash.""" try: - cursor = self.connection.cursor() + with self._db_lock: + cursor = self.connection.cursor() - cursor.execute( - """ - SELECT m.* FROM metadata m - WHERE m.hash = ? - """, - (file_hash, - ), - ) + cursor.execute( + """ + SELECT m.* FROM metadata m + WHERE m.hash = ? + """, + (file_hash, + ), + ) - row = cursor.fetchone() - if not row: - return None + row = cursor.fetchone() + if not row: + return None - metadata = dict(row) + metadata = dict(row) # Parse JSON fields for field in ["url", "relationships"]: @@ -1236,19 +1237,20 @@ class API_folder_store: def get_tags(self, file_hash: str) -> List[str]: """Get all tags for a file by hash.""" try: - cursor = self.connection.cursor() + with self._db_lock: + cursor = self.connection.cursor() - cursor.execute( - """ - SELECT t.tag FROM tag t - WHERE t.hash = ? - ORDER BY t.tag - """, - (file_hash, - ), - ) + cursor.execute( + """ + SELECT t.tag FROM tag t + WHERE t.hash = ? + ORDER BY t.tag + """, + (file_hash, + ), + ) - return [row[0] for row in cursor.fetchall()] + return [row[0] for row in cursor.fetchall()] except Exception as e: logger.error(f"Error getting tags for hash {file_hash}: {e}", exc_info=True) return [] @@ -1833,18 +1835,19 @@ class API_folder_store: def search_hash(self, file_hash: str) -> Optional[Path]: """Search for a file by hash.""" try: - cursor = self.connection.cursor() + with self._db_lock: + cursor = self.connection.cursor() - cursor.execute( - """ - SELECT file_path FROM file WHERE hash = ? - """, - (file_hash, - ), - ) + cursor.execute( + """ + SELECT file_path FROM file WHERE hash = ? + """, + (file_hash, + ), + ) - row = cursor.fetchone() - return self._from_db_file_path(row[0]) if row else None + row = cursor.fetchone() + return self._from_db_file_path(row[0]) if row else None except Exception as e: logger.error(f"Error searching by hash '{file_hash}': {e}", exc_info=True) return None @@ -3525,13 +3528,15 @@ class LocalLibrarySearchOptimizer: """Get tags from database cache.""" if not self.db: return [] - return self.db.get_tags(file_path) + file_hash = self.db.get_file_hash(file_path) + return self.db.get_tags(file_hash) if file_hash else [] def get_cached_metadata(self, file_path: Path) -> Optional[Dict[str, Any]]: """Get metadata from database cache.""" if not self.db: return None - return self.db.get_metadata(file_path) + file_hash = self.db.get_file_hash(file_path) + return self.db.get_metadata(file_hash) if file_hash else None def prefetch_metadata(self, file_paths: List[Path]) -> None: """Pre-cache metadata for multiple files.""" @@ -3554,11 +3559,15 @@ class LocalLibrarySearchOptimizer: return try: - tags = self.db.get_tags(file_path) + file_hash = self.db.get_file_hash(file_path) + if not file_hash: + return + + tags = self.db.get_tags(file_hash) if tags: search_result.tag_summary = ", ".join(tags) - metadata = self.db.get_metadata(file_path) + metadata = self.db.get_metadata(file_hash) if metadata: if "hash" in metadata: search_result.hash_hex = metadata["hash"] @@ -3575,27 +3584,28 @@ class LocalLibrarySearchOptimizer: return [] try: - cursor = self.db.connection.cursor() - cursor.execute( - """ - SELECT f.hash, f.file_path, m.duration, m.size, m.type as media_kind, m.url - FROM file f - JOIN tag t ON f.hash = t.hash - LEFT JOIN metadata m ON f.hash = m.hash - WHERE t.tag LIKE ? - LIMIT ? - """, - (f"%{tag}%", - limit), - ) + with self.db._db_lock: + cursor = self.db.connection.cursor() + cursor.execute( + """ + SELECT f.hash, f.file_path, m.duration, m.size, m.type as media_kind, m.url + FROM file f + JOIN tag t ON f.hash = t.hash + LEFT JOIN metadata m ON f.hash = m.hash + WHERE t.tag LIKE ? + LIMIT ? + """, + (f"%{tag}%", + limit), + ) - results = [] - for row in cursor.fetchall(): - res = dict(row) - # Resolve path to absolute string for remote consumption - res["file_path"] = str(self.db._from_db_file_path(res["file_path"])) - results.append(res) - return results + results = [] + for row in cursor.fetchall(): + res = dict(row) + # Resolve path to absolute string for remote consumption + res["file_path"] = str(self.db._from_db_file_path(res["file_path"])) + results.append(res) + return results except Exception as e: logger.error(f"Tag search failed: {e}") return [] @@ -3606,26 +3616,27 @@ class LocalLibrarySearchOptimizer: return [] try: - cursor = self.db.connection.cursor() - cursor.execute( - """ - SELECT f.hash, f.file_path, m.duration, m.size, m.type as media_kind, m.url - FROM file f - LEFT JOIN metadata m ON f.hash = m.hash - WHERE f.file_path LIKE ? - LIMIT ? - """, - (f"%{query}%", - limit), - ) + with self.db._db_lock: + cursor = self.db.connection.cursor() + cursor.execute( + """ + SELECT f.hash, f.file_path, m.duration, m.size, m.type as media_kind, m.url + FROM file f + LEFT JOIN metadata m ON f.hash = m.hash + WHERE f.file_path LIKE ? + LIMIT ? + """, + (f"%{query}%", + limit), + ) - results = [] - for row in cursor.fetchall(): - res = dict(row) - # Resolve path to absolute string for remote consumption - res["file_path"] = str(self.db._from_db_file_path(res["file_path"])) - results.append(res) - return results + results = [] + for row in cursor.fetchall(): + res = dict(row) + # Resolve path to absolute string for remote consumption + res["file_path"] = str(self.db._from_db_file_path(res["file_path"])) + results.append(res) + return results except Exception as e: logger.error(f"Name search failed: {e}") return [] diff --git a/scripts/remote_storage_server.py b/scripts/remote_storage_server.py index 5a8d22e..65ffcd1 100644 --- a/scripts/remote_storage_server.py +++ b/scripts/remote_storage_server.py @@ -69,6 +69,17 @@ logger = logging.getLogger(__name__) STORAGE_PATH: Optional[Path] = None API_KEY: Optional[str] = None # API key for authentication (None = no auth required) +# Cache for database connection to prevent "database is locked" on high frequency requests +_DB_CACHE: Dict[str, Any] = {} + +def get_db(path: Path): + from API.folder import LocalLibrarySearchOptimizer + p_str = str(path) + if p_str not in _DB_CACHE: + _DB_CACHE[p_str] = LocalLibrarySearchOptimizer(path) + _DB_CACHE[p_str].__enter__() + return _DB_CACHE[p_str] + # Try importing Flask - will be used in main() only try: from flask import Flask, request, jsonify @@ -199,25 +210,34 @@ def create_app(): # ======================================================================== @app.route("/health", methods=["GET"]) - @require_auth() def health(): """Check server health and storage availability.""" + # Check auth manually to allow discovery even if locked + authed = True + if API_KEY: + provided_key = request.headers.get("X-API-Key") or request.args.get("api_key") + if not provided_key or provided_key != API_KEY: + authed = False + status = { "status": "ok", "service": "remote_storage", "name": os.environ.get("MM_SERVER_NAME", "Remote Storage"), "storage_configured": STORAGE_PATH is not None, "timestamp": datetime.now().isoformat(), + "locked": not authed and API_KEY is not None } + # If not authed but API_KEY is required, return minimal info for discovery + if not authed and API_KEY: + return jsonify(status), 200 + if STORAGE_PATH: status["storage_path"] = str(STORAGE_PATH) status["storage_exists"] = STORAGE_PATH.exists() try: - from API.folder import API_folder_store - - with API_folder_store(STORAGE_PATH) as db: - status["database_accessible"] = True + search_db = get_db(STORAGE_PATH) + status["database_accessible"] = True except Exception as e: status["database_accessible"] = False status["database_error"] = str(e) @@ -233,8 +253,6 @@ def create_app(): @require_storage() def search_files(): """Search for files by name or tag.""" - from API.folder import LocalLibrarySearchOptimizer, API_folder_store - query = request.args.get("q", "") limit = request.args.get("limit", 100, type=int) @@ -242,30 +260,32 @@ def create_app(): db_query = query if query and query != "*" else "" try: - with LocalLibrarySearchOptimizer(STORAGE_PATH) as search_db: - results = search_db.search_by_name(db_query, limit) - tag_results = search_db.search_by_tag(db_query, limit) - all_results_dict = { - r["hash"]: r - for r in (results + tag_results) - } + search_db = get_db(STORAGE_PATH) + results = search_db.search_by_name(db_query, limit) + tag_results = search_db.search_by_tag(db_query, limit) + all_results_dict = { + r["hash"]: r + for r in (results + tag_results) + } - # Fetch tags for each result to support title extraction on client - with API_folder_store(STORAGE_PATH) as db: - for res in all_results_dict.values(): - if res.get("file_path"): - res["tag"] = db.get_tags(Path(res["file_path"])) + # Fetch tags for each result to support title extraction on client + if search_db.db: + for res in all_results_dict.values(): + file_hash = res.get("hash") + if file_hash: + tags = search_db.db.get_tags(file_hash) + res["tag"] = tags - return ( - jsonify( - { - "query": query, - "count": len(all_results_dict), - "files": list(all_results_dict.values()), - } - ), - 200, - ) + return ( + jsonify( + { + "query": query, + "count": len(all_results_dict), + "files": list(all_results_dict.values()), + } + ), + 200, + ) except Exception as e: logger.error(f"Search error: {e}", exc_info=True) return jsonify({"error": f"Search failed: {str(e)}"}), 500 @@ -275,30 +295,32 @@ def create_app(): @require_storage() def get_file_metadata(file_hash: str): """Get metadata for a specific file by hash.""" - from API.folder import API_folder_store - try: - with API_folder_store(STORAGE_PATH) as db: - file_path = db.search_hash(file_hash) + search_db = get_db(STORAGE_PATH) + db = search_db.db + if not db: + return jsonify({"error": "Database unavailable"}), 500 + + file_path = db.search_hash(file_hash) - if not file_path or not file_path.exists(): - return jsonify({"error": "File not found"}), 404 + if not file_path or not file_path.exists(): + return jsonify({"error": "File not found"}), 404 - metadata = db.get_metadata(file_path) - tags = db.get_tags(file_path) + metadata = db.get_metadata(file_hash) + tags = db.get_tags(file_hash) # Use hash string - return ( - jsonify( - { - "hash": file_hash, - "path": str(file_path), - "size": file_path.stat().st_size, - "metadata": metadata, - "tag": tags, - } - ), - 200, - ) + return ( + jsonify( + { + "hash": file_hash, + "path": str(file_path), + "size": file_path.stat().st_size, + "metadata": metadata, + "tag": tags, + } + ), + 200, + ) except Exception as e: logger.error(f"Get metadata error: {e}", exc_info=True) return jsonify({"error": f"Failed to get metadata: {str(e)}"}), 500