diff --git a/plugins/douyu/main.py b/plugins/douyu/main.py index 3b2d60e..5d7a039 100644 --- a/plugins/douyu/main.py +++ b/plugins/douyu/main.py @@ -891,18 +891,18 @@ class DouyuPlugin(MessagePluginInterface): @staticmethod def _normalize_audience_points(points: List[Dict[str, Any]], limit: int = 720) -> List[Dict[str, Any]]: - normalized: List[Dict[str, Any]] = [] - seen = set() + normalized_map: Dict[str, Dict[str, Any]] = {} for item in points or []: timestamp = str(item.get("timestamp") or "").strip() - if not timestamp or timestamp in seen: + if not timestamp: continue - seen.add(timestamp) - normalized.append({ + minute_key = timestamp[:16] + normalized_map[minute_key] = { "timestamp": timestamp, "vip_count": int(item.get("vip_count", 0) or 0), "diamond_count": int(item.get("diamond_count", 0) or 0), - }) + } + normalized = list(normalized_map.values()) normalized.sort(key=lambda row: row.get("timestamp", "")) if len(normalized) > limit: normalized = normalized[-limit:] @@ -1183,7 +1183,16 @@ class DouyuPlugin(MessagePluginInterface): def _build_audience_trend(self, sessions: List[Dict[str, Any]]) -> Dict[str, Any]: points: List[Dict[str, Any]] = [] + segment_start_times: List[str] = [] + segment_end_times: List[str] = [] for session in sessions: + for segment in session.get("segments", []) or []: + start_time = str(segment.get("start_time") or "").strip() + end_time = str(segment.get("end_time") or "").strip() + if start_time: + segment_start_times.append(start_time) + if end_time: + segment_end_times.append(end_time) for item in session.get("audience_points", []) or []: point = { "timestamp": str(item.get("timestamp") or "").strip(), @@ -1198,6 +1207,16 @@ class DouyuPlugin(MessagePluginInterface): vip_values = [int(item.get("vip_count", 0) or 0) for item in points] diamond_values = [int(item.get("diamond_count", 0) or 0) for item in points] labels = [str(item.get("timestamp") or "")[-8:-3] for item in points] + session_start = min(segment_start_times) if segment_start_times else "" + session_end = max(segment_end_times) if segment_end_times else str(points[-1].get("timestamp") or "") + first_point_time = str(points[0].get("timestamp") or "") + last_point_time = str(points[-1].get("timestamp") or "") + leading_gap_minutes = 0 + if session_start and first_point_time: + start_dt = self._parse_session_time(session_start) + point_dt = self._parse_session_time(first_point_time) + if start_dt and point_dt: + leading_gap_minutes = max(int((point_dt - start_dt).total_seconds() // 60), 0) return { "points": points, "summary": { @@ -1209,6 +1228,11 @@ class DouyuPlugin(MessagePluginInterface): "diamond_max": max(diamond_values), "diamond_latest": diamond_values[-1], "labels": labels, + "session_start": session_start, + "session_end": session_end, + "first_point_time": first_point_time, + "last_point_time": last_point_time, + "leading_gap_minutes": leading_gap_minutes, }, } diff --git a/plugins/douyu/report_template.py b/plugins/douyu/report_template.py index 539e45e..16dc80f 100644 --- a/plugins/douyu/report_template.py +++ b/plugins/douyu/report_template.py @@ -209,28 +209,56 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str: plot_width = width - padding_left - padding_right plot_height = height - padding_top - padding_bottom - vip_values = [int(item.get("vip_count", 0) or 0) for item in points] - diamond_values = [int(item.get("diamond_count", 0) or 0) for item in points] - labels = [str(item.get("timestamp") or "")[-8:-3] for item in points] + summary = audience_trend.get("summary", {}) or {} + + def _compress_chart_points(raw_points: List[Dict[str, Any]], max_points: int = 36) -> List[Dict[str, Any]]: + if len(raw_points) <= max_points: + return list(raw_points) + bucket_size = max(len(raw_points) / max_points, 1) + compressed: List[Dict[str, Any]] = [] + for idx in range(max_points): + start = int(round(idx * bucket_size)) + end = int(round((idx + 1) * bucket_size)) + bucket = raw_points[start:end] or raw_points[start:start + 1] + if not bucket: + continue + compressed.append(dict(bucket[-1])) + first = raw_points[0] + last = raw_points[-1] + if compressed: + compressed[0] = dict(first) + compressed[-1] = dict(last) + return compressed + + chart_points = _compress_chart_points(points, max_points=36) + vip_values = [int(item.get("vip_count", 0) or 0) for item in chart_points] + diamond_values = [int(item.get("diamond_count", 0) or 0) for item in chart_points] + labels = [str(item.get("timestamp") or "")[-8:-3] for item in chart_points] vip_min = min(vip_values) vip_max = max(vip_values) diamond_min = min(diamond_values) diamond_max = max(diamond_values) - vip_span = max(vip_max - vip_min, 1) - diamond_span = max(diamond_max - diamond_min, 1) - step_x = plot_width / max(len(points) - 1, 1) + vip_padding = max(int((vip_max - vip_min) * 0.12), 60) + diamond_padding = 1 if diamond_max - diamond_min <= 2 else max(int((diamond_max - diamond_min) * 0.2), 1) + vip_display_min = max(vip_min - vip_padding, 0) + vip_display_max = vip_max + vip_padding + diamond_display_min = max(diamond_min - diamond_padding, 0) + diamond_display_max = diamond_max + diamond_padding + vip_span = max(vip_display_max - vip_display_min, 1) + diamond_span = max(diamond_display_max - diamond_display_min, 1) + step_x = plot_width / max(len(chart_points) - 1, 1) bar_width = max(min(step_x * 0.58, 18), 6) def x_at(index: int) -> float: return padding_left + step_x * index def y_vip(value: int) -> float: - ratio = (value - vip_min) / vip_span if vip_span else 0 + ratio = (value - vip_display_min) / vip_span if vip_span else 0 return padding_top + plot_height - ratio * plot_height def y_diamond(value: int) -> float: - ratio = (value - diamond_min) / diamond_span if diamond_span else 0 + ratio = (value - diamond_display_min) / diamond_span if diamond_span else 0 return padding_top + plot_height - ratio * plot_height grid_lines = [] @@ -239,8 +267,8 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str: for idx in range(5): ratio = idx / 4 y = padding_top + plot_height - ratio * plot_height - vip_tick = round(vip_min + vip_span * ratio) - diamond_tick = round(diamond_min + diamond_span * ratio) + vip_tick = round(vip_display_min + vip_span * ratio) + diamond_tick = round(diamond_display_min + diamond_span * ratio) grid_lines.append( f'' ) @@ -256,14 +284,14 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str: dot_points = [] label_marks = [] annotations = [] - if len(points) <= 12: - label_indexes = list(range(len(points))) + if len(chart_points) <= 12: + label_indexes = list(range(len(chart_points))) else: - label_indexes = sorted(set([0, len(points) - 1] + [int(round((len(points) - 1) * i / 6)) for i in range(1, 6)])) + label_indexes = sorted(set([0, len(chart_points) - 1] + [int(round((len(chart_points) - 1) * i / 6)) for i in range(1, 6)])) vip_peak_index = vip_values.index(vip_max) vip_valley_index = vip_values.index(vip_min) - diamond_latest_index = len(points) - 1 - for idx, item in enumerate(points): + diamond_latest_index = len(chart_points) - 1 + for idx, item in enumerate(chart_points): x = x_at(idx) vip_y = y_vip(int(item.get("vip_count", 0) or 0)) diamond_y = y_diamond(int(item.get("diamond_count", 0) or 0)) @@ -300,9 +328,14 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str: ) polyline = f'' - summary = audience_trend.get("summary", {}) or {} vip_latest = int(summary.get("vip_latest", vip_values[-1]) or 0) diamond_latest = int(summary.get("diamond_latest", diamond_values[-1]) or 0) + session_start = str(summary.get("session_start") or "") + first_point_time = str(summary.get("first_point_time") or "") + leading_gap_minutes = int(summary.get("leading_gap_minutes", 0) or 0) + note_text = "" + if session_start and first_point_time and leading_gap_minutes >= 20: + note_text = f"采样起点 {first_point_time[-8:-3]},早于该时段的人数数据未记录" return f"""
@@ -314,6 +347,7 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str: 最新:贵宾 {_escape(vip_latest)} / 钻粉 {_escape(diamond_latest)}
+ {'
' + _escape(note_text) + '
' if note_text else ''} @@ -744,6 +778,12 @@ def render_daily_report_html( gap: 12px; margin-bottom: 10px; }} + .chart-note {{ + margin: -2px 0 8px; + color: #7b8798; + font-size: 12px; + line-height: 1.5; + }} .chart-title {{ font-size: 17px; font-weight: 800;