feat: improve friend circle like state handling
This commit is contained in:
@@ -2,7 +2,8 @@ import asyncio
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import Blueprint, current_app, jsonify, render_template, request
|
import requests
|
||||||
|
from flask import Blueprint, Response, current_app, jsonify, render_template, request, stream_with_context
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .auth import login_required
|
from .auth import login_required
|
||||||
@@ -15,6 +16,41 @@ def _run_client_coro(server, coro, timeout=90):
|
|||||||
return future.result(timeout=timeout)
|
return future.result(timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def _proxy_remote_media(target_url: str) -> Response:
|
||||||
|
if not target_url:
|
||||||
|
return Response("missing url", status=400)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/123.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
"Referer": "http://weixin.qq.com/"
|
||||||
|
}
|
||||||
|
range_header = request.headers.get("Range")
|
||||||
|
if range_header:
|
||||||
|
headers["Range"] = range_header
|
||||||
|
|
||||||
|
upstream = requests.get(target_url, headers=headers, stream=True, timeout=30)
|
||||||
|
|
||||||
|
response_headers = {}
|
||||||
|
for key in ("Content-Type", "Content-Length", "Content-Range", "Accept-Ranges", "Cache-Control", "ETag", "Last-Modified"):
|
||||||
|
value = upstream.headers.get(key)
|
||||||
|
if value:
|
||||||
|
response_headers[key] = value
|
||||||
|
|
||||||
|
if "Accept-Ranges" not in response_headers:
|
||||||
|
response_headers["Accept-Ranges"] = "bytes"
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
stream_with_context(upstream.iter_content(chunk_size=64 * 1024)),
|
||||||
|
status=upstream.status_code,
|
||||||
|
headers=response_headers,
|
||||||
|
direct_passthrough=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _safe_text(value, default=""):
|
def _safe_text(value, default=""):
|
||||||
if value is None:
|
if value is None:
|
||||||
return default
|
return default
|
||||||
@@ -158,14 +194,17 @@ def _normalize_like(like):
|
|||||||
return {
|
return {
|
||||||
"author_wxid": (
|
"author_wxid": (
|
||||||
_safe_text(like.get("UserName"))
|
_safe_text(like.get("UserName"))
|
||||||
|
or _safe_text(like.get("Username"))
|
||||||
or _safe_text(like.get("username"))
|
or _safe_text(like.get("username"))
|
||||||
or _safe_text(like.get("Wxid"))
|
or _safe_text(like.get("Wxid"))
|
||||||
),
|
),
|
||||||
"author_name": (
|
"author_name": (
|
||||||
_safe_text(like.get("NickName"))
|
_safe_text(like.get("NickName"))
|
||||||
|
or _safe_text(like.get("Nickname"))
|
||||||
or _safe_text(like.get("nickname"))
|
or _safe_text(like.get("nickname"))
|
||||||
or _safe_text(like.get("DisplayName"))
|
or _safe_text(like.get("DisplayName"))
|
||||||
)
|
),
|
||||||
|
"create_time": _safe_text(like.get("CreateTime")) or _safe_text(like.get("createTime"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -238,6 +277,8 @@ def _normalize_post(node, server):
|
|||||||
comment_nodes.extend(_extract_list(node.get(key)))
|
comment_nodes.extend(_extract_list(node.get(key)))
|
||||||
for key in ("LikeList", "Likes", "likeList", "likes"):
|
for key in ("LikeList", "Likes", "likeList", "likes"):
|
||||||
like_nodes.extend(_extract_list(node.get(key)))
|
like_nodes.extend(_extract_list(node.get(key)))
|
||||||
|
for key in ("LikeUserList", "likeUserList"):
|
||||||
|
like_nodes.extend(_extract_list(node.get(key)))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": object_id,
|
"id": object_id,
|
||||||
@@ -246,6 +287,9 @@ def _normalize_post(node, server):
|
|||||||
"author_avatar": server.contact_manager.get_head_image(author_wxid),
|
"author_avatar": server.contact_manager.get_head_image(author_wxid),
|
||||||
"content": content,
|
"content": content,
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
|
"like_flag": _safe_text(node.get("LikeFlag")),
|
||||||
|
"is_liked": _safe_text(node.get("LikeFlag")) in ("1", "true", "True"),
|
||||||
|
"like_count": _safe_text(node.get("LikeCount")),
|
||||||
"location": xml_data.get("location", {}),
|
"location": xml_data.get("location", {}),
|
||||||
"media": [item for item in media if item.get("url") or item.get("thumb")],
|
"media": [item for item in media if item.get("url") or item.get("thumb")],
|
||||||
"comments": [item for item in (_normalize_comment(x) for x in comment_nodes) if item.get("content") or item.get("author_wxid")],
|
"comments": [item for item in (_normalize_comment(x) for x in comment_nodes) if item.get("content") or item.get("author_wxid")],
|
||||||
@@ -445,3 +489,14 @@ def friend_circle_comment():
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"朋友圈评论失败: {exc}")
|
logger.error(f"朋友圈评论失败: {exc}")
|
||||||
return jsonify({"success": False, "message": str(exc)}), 500
|
return jsonify({"success": False, "message": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@friend_circle_bp.route("/api/friend_circle/media_proxy")
|
||||||
|
@login_required
|
||||||
|
def friend_circle_media_proxy():
|
||||||
|
target_url = request.args.get("url", "").strip()
|
||||||
|
try:
|
||||||
|
return _proxy_remote_media(target_url)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"朋友圈媒体代理失败: {exc}")
|
||||||
|
return Response(f"proxy failed: {exc}", status=502)
|
||||||
|
|||||||
@@ -83,8 +83,19 @@
|
|||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<el-button type="text" @click="openRawDialog(post)">原始数据</el-button>
|
<el-button type="text" @click="openRawDialog(post)">原始数据</el-button>
|
||||||
<el-button type="text" @click="openCommentDialog(post)">评论</el-button>
|
<el-button type="text" @click="openCommentDialog(post)">评论</el-button>
|
||||||
<el-button type="text" @click="toggleLike(post, false)">点赞</el-button>
|
<el-button
|
||||||
<el-button type="text" class="danger-text" @click="toggleLike(post, true)">取消赞</el-button>
|
v-if="!post.is_liked"
|
||||||
|
type="text"
|
||||||
|
@click="toggleLike(post, false)">
|
||||||
|
点赞
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
class="danger-text"
|
||||||
|
@click="toggleLike(post, true)">
|
||||||
|
取消点赞
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -104,7 +115,7 @@
|
|||||||
class="post-media-item"
|
class="post-media-item"
|
||||||
:class="{ 'is-video': media.is_video }"
|
:class="{ 'is-video': media.is_video }"
|
||||||
@click="openMediaPreview(media)">
|
@click="openMediaPreview(media)">
|
||||||
<img :src="media.thumb || media.hd || media.url" :alt="post.content || '朋友圈媒体'">
|
<img :src="getMediaProxyUrl(media.thumb || media.hd || media.url)" :alt="post.content || '朋友圈媒体'">
|
||||||
<div v-if="media.is_video" class="video-badge">
|
<div v-if="media.is_video" class="video-badge">
|
||||||
<i class="el-icon-video-play"></i>
|
<i class="el-icon-video-play"></i>
|
||||||
<span>{% raw %}{{ formatVideoDuration(media.video_duration) }}{% endraw %}</span>
|
<span>{% raw %}{{ formatVideoDuration(media.video_duration) }}{% endraw %}</span>
|
||||||
@@ -114,7 +125,7 @@
|
|||||||
|
|
||||||
<div class="post-feedback">
|
<div class="post-feedback">
|
||||||
<div class="feedback-block">
|
<div class="feedback-block">
|
||||||
<div class="feedback-title">点赞 {% raw %}{{ post.likes.length }}{% endraw %}</div>
|
<div class="feedback-title">点赞 {% raw %}{{ post.likes.length || post.like_count || 0 }}{% endraw %}</div>
|
||||||
<div v-if="post.likes.length" class="feedback-list">
|
<div v-if="post.likes.length" class="feedback-list">
|
||||||
<span v-for="(like, index) in post.likes" :key="`${post.id}-like-${index}`" class="feedback-chip">
|
<span v-for="(like, index) in post.likes" :key="`${post.id}-like-${index}`" class="feedback-chip">
|
||||||
{% raw %}{{ like.author_name || like.author_wxid }}{% endraw %}
|
{% raw %}{{ like.author_name || like.author_wxid }}{% endraw %}
|
||||||
@@ -214,8 +225,8 @@
|
|||||||
<img v-if="previewMedia && !previewMedia.is_video && previewImageUrl" :src="previewImageUrl" class="large-preview" alt="朋友圈图片预览">
|
<img v-if="previewMedia && !previewMedia.is_video && previewImageUrl" :src="previewImageUrl" class="large-preview" alt="朋友圈图片预览">
|
||||||
<video
|
<video
|
||||||
v-if="previewMedia && previewMedia.is_video"
|
v-if="previewMedia && previewMedia.is_video"
|
||||||
:src="previewMedia.url"
|
:src="getMediaProxyUrl(previewMedia.url)"
|
||||||
:poster="previewMedia.thumb || previewMedia.hd || previewMedia.url"
|
:poster="getMediaProxyUrl(previewMedia.thumb || previewMedia.hd || previewMedia.url)"
|
||||||
class="large-video"
|
class="large-video"
|
||||||
controls
|
controls
|
||||||
playsinline
|
playsinline
|
||||||
@@ -401,9 +412,16 @@
|
|||||||
},
|
},
|
||||||
openMediaPreview(media) {
|
openMediaPreview(media) {
|
||||||
this.previewMedia = media;
|
this.previewMedia = media;
|
||||||
this.previewImageUrl = media.hd || media.url || media.thumb || '';
|
this.previewImageUrl = this.getMediaProxyUrl(media.hd || media.url || media.thumb || '');
|
||||||
this.imagePreviewVisible = true;
|
this.imagePreviewVisible = true;
|
||||||
},
|
},
|
||||||
|
getMediaProxyUrl(url) {
|
||||||
|
if (!url) return '';
|
||||||
|
if (url.startsWith('/api/friend_circle/media_proxy')) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return `/api/friend_circle/media_proxy?url=${encodeURIComponent(url)}`;
|
||||||
|
},
|
||||||
formatVideoDuration(value) {
|
formatVideoDuration(value) {
|
||||||
const seconds = Math.round(parseFloat(value || 0));
|
const seconds = Math.round(parseFloat(value || 0));
|
||||||
if (!seconds || Number.isNaN(seconds)) return '视频';
|
if (!seconds || Number.isNaN(seconds)) return '视频';
|
||||||
|
|||||||
Reference in New Issue
Block a user