Merge pull request #26 from H3CoF6/feat/sns-reverse

feat (sns): calc img path for sns
This commit is contained in:
2977094657
2026-02-13 13:34:57 +08:00
committed by GitHub
2 changed files with 59 additions and 13 deletions

View File

@@ -609,10 +609,17 @@ const getSnsMediaUrl = (post, m, idx, rawUrl) => {
if (h) parts.set('height', h) if (h) parts.set('height', h)
if (/^\d+$/.test(ts)) parts.set('total_size', ts) if (/^\d+$/.test(ts)) parts.set('total_size', ts)
parts.set('idx', String(Number(sizeIdx) || 0)) parts.set('idx', String(Number(sizeIdx) || 0))
const pid = String(post?.id || '').trim()
if (pid) parts.set('post_id', pid)
const mid = String(m?.id || '').trim()
if (mid) parts.set('media_id', mid)
const mtype = String(m?.type || '').trim()
if (mtype) parts.set('media_type', mtype)
if (pick) parts.set('pick', pick) if (pick) parts.set('pick', pick)
if (!pick && snsAvoidOtherPicked.value) { if (!pick && snsAvoidOtherPicked.value) {
const pid = String(post?.id || '').trim()
if (pid) parts.set('post_id', pid)
parts.set('avoid_picked', '1') parts.set('avoid_picked', '1')
parts.set('pv', String(snsMediaOverrideRev.value || '0')) parts.set('pv', String(snsMediaOverrideRev.value || '0'))
} }

View File

@@ -1,6 +1,7 @@
from bisect import bisect_left, bisect_right from bisect import bisect_left, bisect_right
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
import hashlib
import json import json
import re import re
import sqlite3 import sqlite3
@@ -471,6 +472,21 @@ def _sns_cache_key_from_path(p: Path) -> str:
return _normalize_hex32(key) return _normalize_hex32(key)
def _generate_sns_cache_key(tid: str, media_id: str, media_type: int = 2) -> str:
"""
公式: md5(tid_mediaId_type)
Example: 14852422213384352392_14852422213963625090_2 -> 6d479249ca5a090fab5c42c79bc56b89
"""
if not tid or not media_id:
return ""
raw_key = f"{tid}_{media_id}_{media_type}"
try:
return hashlib.md5(raw_key.encode("utf-8")).hexdigest()
except Exception:
return ""
def _resolve_sns_cached_image_path_by_cache_key( def _resolve_sns_cached_image_path_by_cache_key(
*, *,
wxid_dir: Path, wxid_dir: Path,
@@ -944,19 +960,42 @@ def list_sns_media_candidates(
@router.get("/api/sns/media", summary="获取朋友圈图片(本地缓存优先)") @router.get("/api/sns/media", summary="获取朋友圈图片(本地缓存优先)")
async def get_sns_media( async def get_sns_media(
account: Optional[str] = None, account: Optional[str] = None,
create_time: int = 0, create_time: int = 0,
width: int = 0, width: int = 0,
height: int = 0, height: int = 0,
total_size: int = 0, total_size: int = 0,
idx: int = 0, idx: int = 0,
avoid_picked: int = 0, avoid_picked: int = 0,
post_id: Optional[str] = None, post_id: Optional[str] = None,
pick: Optional[str] = None, media_id: Optional[str] = None,
md5: Optional[str] = None, media_type: int = 2,
url: Optional[str] = None, pick: Optional[str] = None,
md5: Optional[str] = None,
url: Optional[str] = None,
): ):
account_dir = _resolve_account_dir(account) account_dir = _resolve_account_dir(account)
wxid_dir = _resolve_account_wxid_dir(account_dir)
if wxid_dir and post_id and media_id:
deterministic_key = _generate_sns_cache_key(post_id, media_id, media_type)
exact_match_path = _resolve_sns_cached_image_path_by_cache_key(
wxid_dir=wxid_dir,
cache_key=deterministic_key,
create_time=0
)
if exact_match_path:
try:
payload, mtype = _read_and_maybe_decrypt_media(Path(exact_match_path), account_dir)
if payload and str(mtype or "").startswith("image/"):
resp = Response(content=payload, media_type=str(mtype or "image/jpeg"))
resp.headers["Cache-Control"] = "public, max-age=31536000" # 确定性缓存可以设置很久
resp.headers["X-SNS-Source"] = "deterministic-hash"
return resp
except Exception:
pass
# 0) User-picked cache key override (stable across candidate ordering). # 0) User-picked cache key override (stable across candidate ordering).
pick_key = _normalize_hex32(pick) pick_key = _normalize_hex32(pick)