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