update
This commit is contained in:
parent
5504b1160d
commit
e31df1e3db
10
README.md
10
README.md
@ -40,8 +40,9 @@ A FastAPI application with Docker and docker-compose support.
|
|||||||
2. **Configure BoxAPI (for Instagram scraping):**
|
2. **Configure BoxAPI (for Instagram scraping):**
|
||||||
```bash
|
```bash
|
||||||
# Add your BoxAPI credentials to .env
|
# Add your BoxAPI credentials to .env
|
||||||
BOXAPI_KEY=your_boxapi_bearer_token
|
BOXAPI_USERNAME=your_boxapi_username
|
||||||
BOXAPI_BASE=https://boxapi.ir
|
BOXAPI_PASSWORD=your_boxapi_password
|
||||||
|
BOXAPI_ENDPOINT=https://boxapi.ir/api/instagram/user/get_media_by_username
|
||||||
BOXAPI_LOG_DIR=/app/logs/boxapi
|
BOXAPI_LOG_DIR=/app/logs/boxapi
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -176,8 +177,9 @@ FastApi-ISS/
|
|||||||
The application uses environment variables for configuration. Copy `env.example` to `.env` and customize the values:
|
The application uses environment variables for configuration. Copy `env.example` to `.env` and customize the values:
|
||||||
|
|
||||||
### BoxAPI Configuration (Instagram Scraping)
|
### BoxAPI Configuration (Instagram Scraping)
|
||||||
- `BOXAPI_KEY`: Your BoxAPI authentication key (Bearer token)
|
- `BOXAPI_USERNAME`: Your BoxAPI username
|
||||||
- `BOXAPI_BASE`: BoxAPI base URL (default: `https://boxapi.ir`)
|
- `BOXAPI_PASSWORD`: Your BoxAPI password
|
||||||
|
- `BOXAPI_ENDPOINT`: BoxAPI endpoint URL (default: `https://boxapi.ir/api/instagram/user/get_media_by_username`)
|
||||||
- `BOXAPI_LOG_DIR`: Directory for BoxAPI response logs (default: `/app/logs/boxapi`)
|
- `BOXAPI_LOG_DIR`: Directory for BoxAPI response logs (default: `/app/logs/boxapi`)
|
||||||
|
|
||||||
### Database Configuration
|
### Database Configuration
|
||||||
|
|||||||
@ -2,60 +2,81 @@ import os, requests
|
|||||||
from typing import Any, Dict, List, Tuple, Optional
|
from typing import Any, Dict, List, Tuple, Optional
|
||||||
from utils.boxapi_log import log_boxapi_response
|
from utils.boxapi_log import log_boxapi_response
|
||||||
|
|
||||||
BOXAPI_BASE = os.environ.get("BOXAPI_BASE", "https://boxapi.ir")
|
BOXAPI_USERNAME = os.environ.get("BOXAPI_USERNAME", "")
|
||||||
BOXAPI_KEY = os.environ.get("BOXAPI_KEY", "") # put your key in .env
|
BOXAPI_PASSWORD = os.environ.get("BOXAPI_PASSWORD", "")
|
||||||
|
# Your .env can override to /get_media; default matches your cURL (/get_media_by_username)
|
||||||
|
BOXAPI_ENDPOINT = os.environ.get(
|
||||||
|
"BOXAPI_ENDPOINT",
|
||||||
|
"https://boxapi.ir/api/instagram/user/get_media_by_username"
|
||||||
|
)
|
||||||
|
|
||||||
class BoxAPIError(Exception):
|
class BoxAPIError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_media(username: str, count: int = 5) -> Tuple[List[Dict[str, Any]], str]:
|
def get_media(username: str, count: int = 5) -> Tuple[List[Dict[str, Any]], str]:
|
||||||
"""
|
"""
|
||||||
Returns (items, log_path)
|
Calls BoxAPI and returns (items, log_path).
|
||||||
items is the array under response.body.items
|
|
||||||
"""
|
"""
|
||||||
url = f"{BOXAPI_BASE}/api/instagram/user/get_media"
|
if not BOXAPI_USERNAME or not BOXAPI_PASSWORD:
|
||||||
payload = {"username": username, "count": count}
|
raise BoxAPIError("Missing BOXAPI_USERNAME/BOXAPI_PASSWORD in environment.")
|
||||||
headers = {"Authorization": f"Bearer {BOXAPI_KEY}"} if BOXAPI_KEY else {}
|
|
||||||
|
|
||||||
|
payload = {"username": username, "count": count}
|
||||||
resp = None
|
resp = None
|
||||||
resp_json: Optional[Dict[str, Any]] = None
|
resp_json: Optional[Dict[str, Any]] = None
|
||||||
|
resp_text: Optional[str] = None
|
||||||
status_code: Optional[int] = None
|
status_code: Optional[int] = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.post(url, json=payload, headers=headers, timeout=60)
|
resp = requests.post(
|
||||||
|
BOXAPI_ENDPOINT,
|
||||||
|
json=payload,
|
||||||
|
auth=(BOXAPI_USERNAME, BOXAPI_PASSWORD),
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
status_code = resp.status_code
|
status_code = resp.status_code
|
||||||
resp.raise_for_status()
|
resp_text = resp.text
|
||||||
resp_json = resp.json()
|
# Try to parse JSON; if it fails, we'll log text and raise
|
||||||
except Exception as e:
|
try:
|
||||||
log_path = log_boxapi_response(username, payload, resp_json, status_code, note=f"HTTP/parse error: {e}")
|
resp_json = resp.json()
|
||||||
raise BoxAPIError(f"BoxAPI request failed ({e}). See log: {log_path}")
|
except Exception:
|
||||||
|
log_path = log_boxapi_response(
|
||||||
|
username, payload, status_code, response_text=resp_text,
|
||||||
|
note="Non-JSON response"
|
||||||
|
)
|
||||||
|
raise BoxAPIError(f"BoxAPI non-JSON response. See log: {log_path}")
|
||||||
|
|
||||||
# ✅ Real structure according to your sample
|
# Expected shape: status -> response -> status_code + body.items
|
||||||
# {
|
|
||||||
# "status": "done",
|
|
||||||
# "response": {
|
|
||||||
# "status_code": 200,
|
|
||||||
# "content_type": "application/json",
|
|
||||||
# "body": {
|
|
||||||
# "items": [ ... ]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
try:
|
|
||||||
top_status = resp_json.get("status")
|
top_status = resp_json.get("status")
|
||||||
inner = resp_json.get("response") or {}
|
inner = resp_json.get("response") or {}
|
||||||
inner_code = inner.get("status_code")
|
inner_code = inner.get("status_code")
|
||||||
body = inner.get("body") or {}
|
body = inner.get("body") or {}
|
||||||
items = body.get("items") or []
|
items = body.get("items") or []
|
||||||
|
|
||||||
log_path = log_boxapi_response(username, payload, resp_json, inner_code, note="OK")
|
log_path = log_boxapi_response(
|
||||||
|
username, payload, inner_code, response_json=resp_json, note="OK"
|
||||||
|
)
|
||||||
|
|
||||||
if top_status != "done" or inner_code != 200 or not isinstance(items, list):
|
if top_status != "done" or inner_code != 200 or not isinstance(items, list):
|
||||||
raise ValueError(f"Unexpected shape: status={top_status}, inner_code={inner_code}, items_type={type(items)}")
|
# Include a concise snapshot of keys to help debugging
|
||||||
|
snapshot = {
|
||||||
|
"top_status": top_status,
|
||||||
|
"inner_code": inner_code,
|
||||||
|
"has_body": isinstance(body, dict),
|
||||||
|
"items_type": str(type(items)),
|
||||||
|
"items_len": len(items) if isinstance(items, list) else None,
|
||||||
|
}
|
||||||
|
raise BoxAPIError(f"Unexpected response format from BoxAPI ({snapshot}). See log: {log_path}")
|
||||||
|
|
||||||
return items, log_path
|
return items, log_path
|
||||||
|
|
||||||
|
except BoxAPIError:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_path = log_boxapi_response(username, payload, resp_json, status_code, note=f"schema error: {e}")
|
log_path = log_boxapi_response(
|
||||||
raise BoxAPIError(f"Unexpected response format from BoxAPI. See log: {log_path}")
|
username, payload, status_code, response_json=resp_json, response_text=resp_text,
|
||||||
|
note=f"HTTP/parse error: {e}"
|
||||||
|
)
|
||||||
|
raise BoxAPIError(f"BoxAPI request failed ({e}). See log: {log_path}")
|
||||||
|
|
||||||
# Legacy function for backward compatibility
|
# Legacy function for backward compatibility
|
||||||
def fetch_boxapi_posts(username: str, count: int = 5) -> List[Dict]:
|
def fetch_boxapi_posts(username: str, count: int = 5) -> List[Dict]:
|
||||||
|
|||||||
@ -10,8 +10,9 @@ def _ensure_dir(path: str) -> None:
|
|||||||
def log_boxapi_response(
|
def log_boxapi_response(
|
||||||
username: str,
|
username: str,
|
||||||
request_payload: Dict[str, Any],
|
request_payload: Dict[str, Any],
|
||||||
response_json: Optional[Dict[str, Any]],
|
|
||||||
status_code: Optional[int],
|
status_code: Optional[int],
|
||||||
|
response_json: Optional[Dict[str, Any]] = None,
|
||||||
|
response_text: Optional[str] = None,
|
||||||
note: str = ""
|
note: str = ""
|
||||||
) -> str:
|
) -> str:
|
||||||
ts = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
|
ts = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
|
||||||
@ -25,8 +26,9 @@ def log_boxapi_response(
|
|||||||
"username": username,
|
"username": username,
|
||||||
"status_code": status_code,
|
"status_code": status_code,
|
||||||
"note": note,
|
"note": note,
|
||||||
"request_payload": request_payload,
|
"request_payload": request_payload, # does NOT include API credentials
|
||||||
"response": response_json,
|
"response_json": response_json,
|
||||||
|
"response_text": response_text if response_json is None else None,
|
||||||
}
|
}
|
||||||
with open(filepath, "w", encoding="utf-8") as f:
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
json.dump(doc, f, ensure_ascii=False, indent=2)
|
json.dump(doc, f, ensure_ascii=False, indent=2)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user