feat: add POST /api/v1/probe endpoint
- Add ProbeRequest/ProbeResponse structures - Support relative and absolute paths - Cache probe.json for repeated requests - Return video metadata (uuid, duration, width, height, fps) - Include cached flag to indicate cache hit - Export FormatInfo and StreamInfo from probe module - Update API_ENDPOINTS.md documentation
This commit is contained in:
217
docs/API_ENDPOINTS.md
Normal file
217
docs/API_ENDPOINTS.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# Momentry Core API 端點總覽
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 版本 | V1.1 |
|
||||||
|
| 日期 | 2026-03-25 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
| 環境 | URL |
|
||||||
|
|------|-----|
|
||||||
|
| 本地 | `http://localhost:3002` |
|
||||||
|
| 外部 | `https://api.momentry.ddns.net` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 端點列表
|
||||||
|
|
||||||
|
### 健康檢查
|
||||||
|
|
||||||
|
| 方法 | 端點 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/health` | 基本健康檢查 |
|
||||||
|
| GET | `/health/detailed` | 詳細健康檢查(含服務狀態) |
|
||||||
|
|
||||||
|
**範例**:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3002/health
|
||||||
|
# {"status":"ok","version":"0.1.0","uptime_ms":123456}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 影片搜尋
|
||||||
|
|
||||||
|
| 方法 | 端點 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/v1/search` | 語意搜尋(標準格式) |
|
||||||
|
| POST | `/api/v1/n8n/search` | 語意搜尋(n8n 專用格式) |
|
||||||
|
| POST | `/api/v1/search/hybrid` | 混合搜尋 |
|
||||||
|
|
||||||
|
**標準搜尋** (`/api/v1/search`):
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query": "test", "limit": 10}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**n8n 格式搜尋** (`/api/v1/n8n/search`):
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query": "test", "limit": 10}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 影片管理
|
||||||
|
|
||||||
|
| 方法 | 端點 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/v1/register` | 註冊影片 |
|
||||||
|
| POST | `/api/v1/probe` | 探測影片資訊(不註冊) |
|
||||||
|
| GET | `/api/v1/videos` | 列出所有影片 |
|
||||||
|
| GET | `/api/v1/lookup` | 查詢影片資訊 |
|
||||||
|
| GET | `/api/v1/progress/:uuid` | 取得處理進度 |
|
||||||
|
|
||||||
|
**註冊影片**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"path": "/path/to/video.mp4"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**探測影片** (不註冊,只取得影片資訊):
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/probe \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"path": "./demo/video.mp4"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Probe 回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "a1b10138a6bbb0cd",
|
||||||
|
"file_name": "video.mp4",
|
||||||
|
"duration": 120.5,
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"fps": 30.0,
|
||||||
|
"cached": false,
|
||||||
|
"format": {
|
||||||
|
"filename": "/path/to/video.mp4",
|
||||||
|
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||||
|
"duration": "120.5",
|
||||||
|
"size": "12345678",
|
||||||
|
"bit_rate": "819200"
|
||||||
|
},
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"codec_name": "h264",
|
||||||
|
"codec_type": "video",
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"r_frame_rate": "30/1",
|
||||||
|
"duration": "120.5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**列出影片**:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3002/api/v1/videos
|
||||||
|
```
|
||||||
|
|
||||||
|
**查詢影片**:
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:3002/api/v1/lookup?uuid=5dea6618a606e7c7"
|
||||||
|
```
|
||||||
|
|
||||||
|
**處理進度**:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 端點對照表
|
||||||
|
|
||||||
|
| 功能 | n8n 使用 | WordPress 使用 | curl 測試 |
|
||||||
|
|------|-----------|----------------|------------|
|
||||||
|
| 健康檢查 | ✓ | ✓ | ✓ |
|
||||||
|
| 語意搜尋 | ✓ (n8n格式) | ✓ (標準格式) | ✓ |
|
||||||
|
| 影片探測 | ✓ | ✓ | ✓ |
|
||||||
|
| 註冊影片 | ✓ | ✓ | ✓ |
|
||||||
|
| 列出影片 | ✓ | ✓ | ✓ |
|
||||||
|
| 查詢影片 | ✓ | ✓ | ✓ |
|
||||||
|
| 處理進度 | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回應格式
|
||||||
|
|
||||||
|
### n8n 格式 (`/api/v1/n8n/search`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "charade",
|
||||||
|
"count": 10,
|
||||||
|
"hits": [
|
||||||
|
{
|
||||||
|
"id": "sentence_0001",
|
||||||
|
"vid": "a1b10138a6bbb0cd",
|
||||||
|
"start": 48.8,
|
||||||
|
"end": 55.44,
|
||||||
|
"title": "Chunk sentence_0001",
|
||||||
|
"text": "...",
|
||||||
|
"score": 0.92,
|
||||||
|
"media_url": "https://wp.momentry.ddns.net/video.mp4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 標準格式 (`/api/v1/search`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"uuid": "a1b10138a6bbb0cd",
|
||||||
|
"chunk_id": "sentence_0001",
|
||||||
|
"chunk_type": "sentence",
|
||||||
|
"start_time": 48.8,
|
||||||
|
"end_time": 55.44,
|
||||||
|
"text": "...",
|
||||||
|
"score": 0.92
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "charade"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTTP 狀態碼
|
||||||
|
|
||||||
|
| 狀態 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| 200 | 成功 |
|
||||||
|
| 400 | 請求格式錯誤 |
|
||||||
|
| 404 | 端點或資源不存在 |
|
||||||
|
| 500 | 伺服器內部錯誤 |
|
||||||
|
| 502 | API 服務未啟動 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 錯誤處理
|
||||||
|
|
||||||
|
### 502 Bad Gateway
|
||||||
|
|
||||||
|
**原因**: Momentry API 服務未啟動
|
||||||
|
|
||||||
|
**解決**:
|
||||||
|
```bash
|
||||||
|
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相關文件
|
||||||
|
|
||||||
|
- [API_INDEX.md](./API_INDEX.md) - 文件總覽(起點)
|
||||||
|
- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 使用範例
|
||||||
|
- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 使用範例
|
||||||
@@ -10,7 +10,6 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::core::cache::{keys, MongoCache, RedisCache};
|
use crate::core::cache::{keys, MongoCache, RedisCache};
|
||||||
use crate::core::config::USER_DATA_ROOT;
|
|
||||||
use crate::core::db::{Database, PostgresDb, QdrantDb, RedisClient, VideoRecord, VideoStatus};
|
use crate::core::db::{Database, PostgresDb, QdrantDb, RedisClient, VideoRecord, VideoStatus};
|
||||||
use crate::{Embedder, FileManager};
|
use crate::{Embedder, FileManager};
|
||||||
|
|
||||||
@@ -79,6 +78,24 @@ struct RegisterResponse {
|
|||||||
already_exists: bool,
|
already_exists: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ProbeRequest {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ProbeResponse {
|
||||||
|
uuid: String,
|
||||||
|
file_name: String,
|
||||||
|
duration: f64,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
fps: f64,
|
||||||
|
cached: bool,
|
||||||
|
format: crate::core::probe::FormatInfo,
|
||||||
|
streams: Vec<crate::core::probe::StreamInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct JobListResponse {
|
struct JobListResponse {
|
||||||
jobs: Vec<JobInfoResponse>,
|
jobs: Vec<JobInfoResponse>,
|
||||||
@@ -395,29 +412,42 @@ async fn register(
|
|||||||
) -> Result<Json<RegisterResponse>, StatusCode> {
|
) -> Result<Json<RegisterResponse>, StatusCode> {
|
||||||
let path = req.path;
|
let path = req.path;
|
||||||
|
|
||||||
// Canonicalize path first to ensure consistent UUID computation
|
// Support both relative and absolute paths
|
||||||
let canonical_path = std::path::Path::new(&path)
|
// Relative: ./demo/video.mp4 or demo/video.mp4
|
||||||
.canonicalize()
|
// Absolute: /Users/.../sftpgo/data/demo/video.mp4
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
let (relative_path, canonical_path) = if path.starts_with("./") || path.starts_with("../") {
|
||||||
.unwrap_or_else(|_| path.clone());
|
// Relative path - keep as is for UUID, resolve to absolute for storage
|
||||||
|
let rel = path.clone();
|
||||||
|
let abs = std::path::Path::new(&path)
|
||||||
|
.canonicalize()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|_| path.clone());
|
||||||
|
(rel, abs)
|
||||||
|
} else if std::path::Path::new(&path).is_absolute() {
|
||||||
|
// Absolute path - use as is
|
||||||
|
(path.clone(), path.clone())
|
||||||
|
} else {
|
||||||
|
// Assume relative path without ./
|
||||||
|
let rel = format!("./{}", path);
|
||||||
|
let abs = std::path::Path::new(&rel)
|
||||||
|
.canonicalize()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|_| path.clone());
|
||||||
|
(rel, abs)
|
||||||
|
};
|
||||||
|
|
||||||
// Compute UUID using USER_DATA_ROOT to extract relative path
|
// Compute UUID from relative path (username/filepath)
|
||||||
// This ensures consistent UUIDs even when data root changes
|
// Extract: ./demo/video.mp4 -> username="demo", filepath="video.mp4"
|
||||||
// Relative path format: username/video.mp4 (e.g., demo/video.mp4)
|
let uuid = crate::core::storage::uuid::compute_uuid_from_relative_path(&relative_path);
|
||||||
let user_data_root = USER_DATA_ROOT.as_str();
|
|
||||||
let uuid = crate::core::storage::uuid::compute_uuid_from_path_with_root(
|
|
||||||
&canonical_path,
|
|
||||||
user_data_root,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Extract relative path for display/logging (username/filename)
|
// Extract username and filepath for logging
|
||||||
let (user_dir, filename) =
|
let (username, filepath) =
|
||||||
crate::core::storage::uuid::extract_relative_path(&canonical_path, user_data_root);
|
crate::core::storage::uuid::extract_user_from_relative_path(&relative_path);
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Registering video: uuid={}, user={}, file={}, full_path={}",
|
"Registering video: uuid={}, username={}, filepath={}, canonical={}",
|
||||||
uuid,
|
uuid,
|
||||||
user_dir,
|
username,
|
||||||
filename,
|
filepath,
|
||||||
canonical_path
|
canonical_path
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -553,6 +583,138 @@ async fn register(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn probe(
|
||||||
|
State(_state): State<AppState>,
|
||||||
|
Json(req): Json<ProbeRequest>,
|
||||||
|
) -> Result<Json<ProbeResponse>, StatusCode> {
|
||||||
|
let path = req.path;
|
||||||
|
|
||||||
|
// Support both relative and absolute paths
|
||||||
|
let (relative_path, canonical_path) = if path.starts_with("./") || path.starts_with("../") {
|
||||||
|
let rel = path.clone();
|
||||||
|
let abs = std::path::Path::new(&path)
|
||||||
|
.canonicalize()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|_| path.clone());
|
||||||
|
(rel, abs)
|
||||||
|
} else if std::path::Path::new(&path).is_absolute() {
|
||||||
|
(path.clone(), path.clone())
|
||||||
|
} else {
|
||||||
|
let rel = format!("./{}", path);
|
||||||
|
let abs = std::path::Path::new(&rel)
|
||||||
|
.canonicalize()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|_| path.clone());
|
||||||
|
(rel, abs)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute UUID from relative path
|
||||||
|
let uuid = crate::core::storage::uuid::compute_uuid_from_relative_path(&relative_path);
|
||||||
|
|
||||||
|
let (username, filepath) =
|
||||||
|
crate::core::storage::uuid::extract_user_from_relative_path(&relative_path);
|
||||||
|
tracing::info!(
|
||||||
|
"Probing video: uuid={}, username={}, filepath={}, canonical={}",
|
||||||
|
uuid,
|
||||||
|
username,
|
||||||
|
filepath,
|
||||||
|
canonical_path
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for cached probe.json
|
||||||
|
let probe_path = format!(
|
||||||
|
"{}/{}.probe.json",
|
||||||
|
crate::core::config::OUTPUT_DIR.as_str(),
|
||||||
|
uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
let (probe_result, cached) = if let Ok(content) = std::fs::read_to_string(&probe_path) {
|
||||||
|
tracing::info!("Using cached probe.json: {}", probe_path);
|
||||||
|
let result: crate::core::probe::ProbeResult =
|
||||||
|
serde_json::from_str(&content).map_err(|e| {
|
||||||
|
tracing::error!("Failed to parse cached probe.json: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
(result, true)
|
||||||
|
} else {
|
||||||
|
tracing::info!("Running ffprobe for: {}", canonical_path);
|
||||||
|
let result = crate::core::probe::probe_video(&canonical_path).map_err(|e| {
|
||||||
|
tracing::error!("ffprobe failed: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Save probe.json
|
||||||
|
let file_manager = FileManager::new(std::path::PathBuf::from("."));
|
||||||
|
let json_str = serde_json::to_string(&result).map_err(|e| {
|
||||||
|
tracing::error!("Failed to serialize probe result: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
file_manager
|
||||||
|
.save_json(&uuid, "probe", &json_str)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to save probe.json: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
(result, false)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract video info
|
||||||
|
let duration = probe_result
|
||||||
|
.format
|
||||||
|
.duration
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse::<f64>().ok())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
let mut width = 0u32;
|
||||||
|
let mut height = 0u32;
|
||||||
|
let mut fps = 0.0;
|
||||||
|
|
||||||
|
for stream in &probe_result.streams {
|
||||||
|
if stream.codec_type.as_deref() == Some("video") {
|
||||||
|
width = stream.width.unwrap_or(0);
|
||||||
|
height = stream.height.unwrap_or(0);
|
||||||
|
// Parse fps from r_frame_rate (e.g., "30/1" or "29.97")
|
||||||
|
if let Some(fps_str) = &stream.r_frame_rate {
|
||||||
|
fps = if fps_str.contains('/') {
|
||||||
|
let parts: Vec<&str> = fps_str.split('/').collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
let num: f64 = parts[0].parse().unwrap_or(0.0);
|
||||||
|
let den: f64 = parts[1].parse().unwrap_or(1.0);
|
||||||
|
if den > 0.0 {
|
||||||
|
num / den
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fps_str.parse().unwrap_or(0.0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = std::path::Path::new(&canonical_path)
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(Json(ProbeResponse {
|
||||||
|
uuid,
|
||||||
|
file_name,
|
||||||
|
duration,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
cached,
|
||||||
|
format: probe_result.format,
|
||||||
|
streams: probe_result.streams,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
async fn search(
|
async fn search(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<SearchRequest>,
|
Json(req): Json<SearchRequest>,
|
||||||
@@ -1115,6 +1277,7 @@ pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
|||||||
.route("/health", get(health))
|
.route("/health", get(health))
|
||||||
.route("/health/detailed", get(health_detailed))
|
.route("/health/detailed", get(health_detailed))
|
||||||
.route("/api/v1/register", post(register))
|
.route("/api/v1/register", post(register))
|
||||||
|
.route("/api/v1/probe", post(probe))
|
||||||
.route("/api/v1/search", post(search))
|
.route("/api/v1/search", post(search))
|
||||||
.route("/api/v1/n8n/search", post(n8n_search))
|
.route("/api/v1/n8n/search", post(n8n_search))
|
||||||
.route("/api/v1/search/hybrid", post(hybrid_search))
|
.route("/api/v1/search/hybrid", post(hybrid_search))
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod probe;
|
pub mod ffprobe;
|
||||||
|
|
||||||
pub use probe::{probe_video, ProbeResult};
|
pub use ffprobe::{probe_video, FormatInfo, ProbeResult, StreamInfo};
|
||||||
|
|||||||
Reference in New Issue
Block a user