mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 06:10:52 +08:00
feat: calc sns video file path
This commit is contained in:
@@ -111,7 +111,21 @@
|
||||
class="inline-block cursor-pointer relative"
|
||||
@click.stop="onMediaClick(post, post.media[0], 0)"
|
||||
>
|
||||
<video
|
||||
v-if="Number(post.media[0]?.type || 0) === 6"
|
||||
:src="getSnsVideoUrl(post.id, post.media[0].id)"
|
||||
:poster="getMediaThumbSrc(post, post.media[0], 0)"
|
||||
class="rounded-sm max-h-[360px] max-w-full object-cover"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
@loadeddata="onLocalVideoLoaded(post.id, post.media[0].id)"
|
||||
@error="onLocalVideoError(post.id, post.media[0].id)"
|
||||
></video>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:src="getMediaThumbSrc(post, post.media[0], 0)"
|
||||
class="rounded-sm max-h-[360px] object-cover"
|
||||
alt=""
|
||||
@@ -120,7 +134,7 @@
|
||||
@error="onMediaError(post.id, 0)"
|
||||
/>
|
||||
<div
|
||||
v-if="Number(post.media[0]?.type || 0) === 6"
|
||||
v-if="Number(post.media[0]?.type || 0) === 6 && !isLocalVideoLoaded(post.id, post.media[0].id)"
|
||||
class="absolute inset-0 flex items-center justify-center pointer-events-none"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-full bg-black/45 flex items-center justify-center">
|
||||
@@ -146,8 +160,20 @@
|
||||
class="w-[116px] h-[116px] rounded-[2px] overflow-hidden bg-gray-100 border border-gray-200 flex items-center justify-center cursor-pointer relative"
|
||||
@click.stop="onMediaClick(post, m, idx)"
|
||||
>
|
||||
<video
|
||||
v-if="!hasMediaError(post.id, idx) && Number(m?.type || 0) === 6"
|
||||
:src="getSnsVideoUrl(post.id, m.id)"
|
||||
:poster="getMediaThumbSrc(post, m, idx)"
|
||||
class="w-full h-full object-cover"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
@loadeddata="onLocalVideoLoaded(post.id, m.id)"
|
||||
@error="onLocalVideoError(post.id, m.id)"
|
||||
></video>
|
||||
<img
|
||||
v-if="!hasMediaError(post.id, idx) && getMediaThumbSrc(post, m, idx)"
|
||||
v-else-if="!hasMediaError(post.id, idx) && getMediaThumbSrc(post, m, idx)"
|
||||
:src="getMediaThumbSrc(post, m, idx)"
|
||||
class="w-full h-full object-cover"
|
||||
alt=""
|
||||
@@ -155,9 +181,13 @@
|
||||
referrerpolicy="no-referrer"
|
||||
@error="onMediaError(post.id, idx)"
|
||||
/>
|
||||
<!-- 不知道微信朋友圈可不可以发多视频,先这样写吧-->
|
||||
<span v-else class="text-[10px] text-gray-400">图片失败</span>
|
||||
|
||||
<div v-if="Number(m?.type || 0) === 6" class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div
|
||||
v-if="Number(m?.type || 0) === 6 && !isLocalVideoLoaded(post.id, m.id)"
|
||||
class="absolute inset-0 flex items-center justify-center pointer-events-none"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-full bg-black/45 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
@@ -707,6 +737,31 @@ const getMediaPreviewSrc = (post, m, idx = 0) => {
|
||||
return getSnsMediaUrl(post, m, idx, m?.url || m?.thumb)
|
||||
}
|
||||
|
||||
|
||||
const getSnsVideoUrl = (postId, mediaId) => {
|
||||
// 本地缓存视频
|
||||
const acc = String(selectedAccount.value || '').trim()
|
||||
if (!acc || !postId || !mediaId) return ''
|
||||
return `${mediaBase}/api/sns/video?account=${encodeURIComponent(acc)}&post_id=${encodeURIComponent(postId)}&media_id=${encodeURIComponent(mediaId)}`
|
||||
}
|
||||
|
||||
const localVideoStatus = ref({})
|
||||
|
||||
const videoStatusKey = (postId, mediaId) => `${String(postId)}:${String(mediaId)}`
|
||||
|
||||
const onLocalVideoLoaded = (postId, mediaId) => {
|
||||
localVideoStatus.value[videoStatusKey(postId, mediaId)] = 'loaded'
|
||||
}
|
||||
|
||||
const onLocalVideoError = (postId, mediaId) => {
|
||||
localVideoStatus.value[videoStatusKey(postId, mediaId)] = 'error'
|
||||
}
|
||||
|
||||
|
||||
const isLocalVideoLoaded = (postId, mediaId) => {
|
||||
return localVideoStatus.value[videoStatusKey(postId, mediaId)] === 'loaded'
|
||||
}
|
||||
|
||||
// 图片预览 + 候选匹配选择
|
||||
const previewCtx = ref(null) // { post, media, idx }
|
||||
const previewCandidatesOpen = ref(false)
|
||||
@@ -902,13 +957,23 @@ const loadMorePreviewCandidates = async () => {
|
||||
const onMediaClick = (post, m, idx = 0) => {
|
||||
if (!process.client) return
|
||||
const mt = Number(m?.type || 0)
|
||||
// 视频:打开视频链接(新窗口),图片:打开预览
|
||||
|
||||
// 视频点击逻辑
|
||||
if (mt === 6) {
|
||||
// 1. 如果本地缓存加载成功,永远不请求 CDN!直接在新标签页打开本地的高清完整视频
|
||||
if (isLocalVideoLoaded(post.id, m.id)) {
|
||||
const localUrl = getSnsVideoUrl(post.id, m.id)
|
||||
window.open(localUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 如果本地没有缓存,按原逻辑 fallback 到 CDN
|
||||
const u = String(m?.url || '').trim()
|
||||
if (u) window.open(u, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
// Open preview overlay; it also loads local candidates for manual selection.
|
||||
if (u) window.open(u, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
|
||||
// 图片:打开预览
|
||||
void openImagePreview(post, m, idx)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import xml.etree.ElementTree as ET
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import Response
|
||||
from fastapi.responses import Response, FileResponse # 返回视频文件
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..chat_helpers import _load_contact_rows, _pick_display_name, _resolve_account_dir
|
||||
@@ -448,6 +448,58 @@ def _sns_img_roots(wxid_dir_str: str) -> tuple[str, ...]:
|
||||
roots.sort()
|
||||
return tuple(roots)
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _sns_video_roots(wxid_dir_str: str) -> tuple[str, ...]:
|
||||
"""List all month cache roots that contain `Sns/Video`."""
|
||||
wxid_dir = Path(str(wxid_dir_str or "").strip())
|
||||
cache_root = wxid_dir / "cache"
|
||||
try:
|
||||
month_dirs = [p for p in cache_root.iterdir() if p.is_dir()]
|
||||
except Exception:
|
||||
month_dirs = []
|
||||
|
||||
roots: list[str] = []
|
||||
for mdir in month_dirs:
|
||||
video_root = mdir / "Sns" / "Video"
|
||||
try:
|
||||
if video_root.exists() and video_root.is_dir():
|
||||
roots.append(str(video_root))
|
||||
except Exception:
|
||||
continue
|
||||
roots.sort()
|
||||
return tuple(roots)
|
||||
|
||||
def _resolve_sns_cached_video_path(
|
||||
wxid_dir: Path,
|
||||
post_id: str,
|
||||
media_id: str
|
||||
) -> Optional[str]:
|
||||
"""基于逆向出的固定盐值 3,解析朋友圈视频的本地缓存路径"""
|
||||
if not post_id or not media_id:
|
||||
return None
|
||||
|
||||
raw_key = f"{post_id}_{media_id}_3" # 暂时硬编码,大概率是对的
|
||||
try:
|
||||
key32 = hashlib.md5(raw_key.encode("utf-8")).hexdigest()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
sub = key32[:2]
|
||||
rest = key32[2:]
|
||||
|
||||
roots = _sns_video_roots(str(wxid_dir))
|
||||
for root_str in roots:
|
||||
try:
|
||||
base_path = Path(root_str) / sub / rest
|
||||
for ext in [".mp4", ".tmp"]:
|
||||
p = base_path.with_suffix(ext)
|
||||
if p.exists() and p.is_file():
|
||||
return str(p)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_sns_cached_image_path_by_md5(
|
||||
*,
|
||||
@@ -1184,4 +1236,27 @@ async def proxy_article_thumb(url: str):
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[sns] 提取公众号封面失败 url={u[:50]}... : {e}")
|
||||
raise HTTPException(status_code=404, detail="无法获取文章封面")
|
||||
raise HTTPException(status_code=404, detail="无法获取文章封面")
|
||||
|
||||
|
||||
@router.get("/api/sns/video", summary="获取朋友圈本地缓存视频")
|
||||
async def get_sns_video(
|
||||
account: Optional[str] = None,
|
||||
post_id: Optional[str] = None,
|
||||
media_id: Optional[str] = None,
|
||||
):
|
||||
if not post_id or not media_id:
|
||||
raise HTTPException(status_code=400, detail="Missing post_id or media_id")
|
||||
|
||||
account_dir = _resolve_account_dir(account)
|
||||
wxid_dir = _resolve_account_wxid_dir(account_dir)
|
||||
|
||||
if not wxid_dir:
|
||||
raise HTTPException(status_code=404, detail="WXID dir not found")
|
||||
|
||||
video_path = _resolve_sns_cached_video_path(wxid_dir, post_id, media_id)
|
||||
|
||||
if not video_path:
|
||||
raise HTTPException(status_code=404, detail="Local video cache not found")
|
||||
|
||||
return FileResponse(video_path, media_type="video/mp4")
|
||||
Reference in New Issue
Block a user