feat(web): default video sort to published and UP sort to avg_views
This commit is contained in:
1 parent
764bde65d9
commit
11930db2bf
8 files changed
+31
-20
No files matched your search
+4
-4
@@ -541,11 +541,11 @@ P3 实现说明:
|
|||||||
| 方法 | 路径 | 用途 |
|
| 方法 | 路径 | 用途 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GET` | `/api/overview` | 首页动态、总览指标、容量、采集状态 |
|
| `GET` | `/api/overview` | 首页动态、总览指标、容量、采集状态 |
|
||||||
| `GET` | `/api/videos` | 视频分页、筛选和排序(`sort` / `within_days` / `year` / `q`;支持 added/published/heat/七项/互动/增长/内容效率) |
|
| `GET` | `/api/videos` | 视频分页、筛选和排序(`sort` / `within_days` / `year` / `q`;支持 added/published/heat/七项/互动/增长/内容效率;默认 `published`) |
|
||||||
| `GET` | `/api/videos/{aid}` | 视频详情(含分 P 数、入库时粉丝数、内容/互动效率) |
|
| `GET` | `/api/videos/{aid}` | 视频详情(含分 P 数、入库时粉丝数、内容/互动效率) |
|
||||||
| `GET` | `/api/videos/{aid}/trend` | 七项趋势数据 |
|
| `GET` | `/api/videos/{aid}/trend` | 七项趋势数据 |
|
||||||
| `GET` | `/api/rankings` | 兼容旧链接;`scope=up` 时为 UP 聚合榜(`year` 可用) |
|
| `GET` | `/api/rankings` | 兼容旧链接;`scope=up` 时为 UP 聚合榜(`year` 可用) |
|
||||||
| `GET` | `/api/ups` | UP 聚合榜单(含总时长、播放·小时、内容效率、UP 头像 `up_face`、最新粉丝数 `follower_latest`;`sort` / `within_days` / `year`) |
|
| `GET` | `/api/ups` | UP 聚合榜单(含总时长、播放·小时、内容效率、UP 头像 `up_face`、最新粉丝数 `follower_latest`;`sort` / `within_days` / `year`;默认 `avg_views`) |
|
||||||
| `GET` | `/api/ups/{mid}` | UP 详情、聚合趋势、头像和视频列表 |
|
| `GET` | `/api/ups/{mid}` | UP 详情、聚合趋势、头像和视频列表 |
|
||||||
| `GET` | `/api/fans` | 粉丝曲线与增量 |
|
| `GET` | `/api/fans` | 粉丝曲线与增量 |
|
||||||
| `GET` | `/api/candidates` | 候选池(只读) |
|
| `GET` | `/api/candidates` | 候选池(只读) |
|
||||||
@@ -559,8 +559,8 @@ P3 实现说明:
|
|||||||
| 页面 | 内容 |
|
| 页面 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 首页 | 最新动态优先:标题下显示「收录视频 N」;顶部四个数据卡片为「今日入库→视频榜按入库排序 / 播放增量→视频榜按 1 日增长排序 / 7 日粉丝变化→粉丝页 / 数据日期」;下方为最近发布、增长动态、热度速览三个区块,统一使用竖排媒体卡(封面 + 两行截断标题 + 作者/数据行 + 高亮数据行)与响应式网格:移动端每行 2 个、PC 每行 3 个,每处各取前 6 条;采集状态等运行期信息只在登录后的「系统」页可见;有候选且已登录时,首页显示「N 条候选待确认」提示条(访客不显示)。 |
|
| 首页 | 最新动态优先:标题下显示「收录视频 N」;顶部四个数据卡片为「今日入库→视频榜按入库排序 / 播放增量→视频榜按 1 日增长排序 / 7 日粉丝变化→粉丝页 / 数据日期」;下方为最近发布、增长动态、热度速览三个区块,统一使用竖排媒体卡(封面 + 两行截断标题 + 作者/数据行 + 高亮数据行)与响应式网格:移动端每行 2 个、PC 每行 3 个,每处各取前 6 条;采集状态等运行期信息只在登录后的「系统」页可见;有候选且已登录时,首页显示「N 条候选待确认」提示条(访客不显示)。 |
|
||||||
| 视频(`/videos`) | 内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量、播放增速;工具条为「排序」下拉 + 时间下拉 + 右侧放大镜搜索框(一行展示),时间支持全部/7 天/30 天/90 天/1 年/年份。 |
|
| 视频(`/videos`) | 发布时间(默认)、内容效率、综合热度、入库时间、播放/投币/收藏/评论、互动率、播放增量、播放增速;工具条为「排序」下拉 + 时间下拉 + 右侧放大镜搜索框(一行展示),时间支持全部/7 天/30 天/90 天/1 年/年份。 |
|
||||||
| UP主(`/ups`) | 独立入口,不再与视频混排;卡片显示 UP 头像、视频数与总时长、最新粉丝数,以及当前排序的主/副指标(「总播放」排序时副指标为「平均播放」);排序下拉含内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布;不提供按视频数筛选。 |
|
| UP主(`/ups`) | 独立入口,不再与视频混排;卡片显示 UP 头像、视频数与总时长、最新粉丝数,以及当前排序的主/副指标(「总播放」排序时副指标为「平均播放」);排序下拉含平均播放(默认)、内容效率、互动效率、内容量、总播放、视频数、总互动、最近发布;不提供按视频数筛选。 |
|
||||||
| 数据说明(`/about`) | 数据来源与更新、发布时间/入库时间/快照日期、指标公式、时长与成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取。 |
|
| 数据说明(`/about`) | 数据来源与更新、发布时间/入库时间/快照日期、指标公式、时长与成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取。 |
|
||||||
| 视频详情 | 元数据、分 P 数、七项指标、内容/互动效率、入库时粉丝数、单指标趋势、快照表、同 UP 对比。 |
|
| 视频详情 | 元数据、分 P 数、七项指标、内容/互动效率、入库时粉丝数、单指标趋势、快照表、同 UP 对比。 |
|
||||||
| UP 页 | 该 UP 的头像、首次观看与最近观看时间、全部视频(默认按发布时间排序)、聚合趋势、单视频对比、总时长与播放·小时/互动·小时、入库时与当前粉丝数。 |
|
| UP 页 | 该 UP 的头像、首次观看与最近观看时间、全部视频(默认按发布时间排序)、聚合趋势、单视频对比、总时长与播放·小时/互动·小时、入库时与当前粉丝数。 |
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ uv run python app.py serve --preview --config data/preview/config.jsonc
|
|||||||
|
|
||||||
- 首页默认“最新动态”:标题下显示「收录视频 N」,顶部四个数据卡片为「今日入库 / 播放增量 / 7 日粉丝变化 / 数据日期」(前三项可点击跳转);下方为最近发布、增长动态、热度速览三个区块,统一使用竖排卡片(封面 + 两行截断标题 + 作者/数据行 + 高亮数据行),并统一为响应式网格(移动端每行 2 个、PC 每行 3 个),每处各取前 6 条;采集状态等运行期信息只在登录后的「系统」页可见;有候选且已登录时,首页显示「N 条候选待确认」提示条(访客不显示);
|
- 首页默认“最新动态”:标题下显示「收录视频 N」,顶部四个数据卡片为「今日入库 / 播放增量 / 7 日粉丝变化 / 数据日期」(前三项可点击跳转);下方为最近发布、增长动态、热度速览三个区块,统一使用竖排卡片(封面 + 两行截断标题 + 作者/数据行 + 高亮数据行),并统一为响应式网格(移动端每行 2 个、PC 每行 3 个),每处各取前 6 条;采集状态等运行期信息只在登录后的「系统」页可见;有候选且已登录时,首页显示「N 条候选待确认」提示条(访客不显示);
|
||||||
- 「视频」与「UP主」是两个独立入口;工具条顺序为「排序」下拉 → 时间下拉(无需 label)→ 右侧自适应搜索框(最大 320px,放大镜按钮在输入框内),三者在 PC 与移动端都保持一行;
|
- 「视频」与「UP主」是两个独立入口;工具条顺序为「排序」下拉 → 时间下拉(无需 label)→ 右侧自适应搜索框(最大 320px,放大镜按钮在输入框内),三者在 PC 与移动端都保持一行;
|
||||||
- 视频排序:内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量/播放增速;UP 排序:内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布;
|
- 视频排序:发布时间(默认)、内容效率、综合热度、入库时间、播放/投币/收藏/评论、互动率、播放增量/播放增速;UP 排序:平均播放(默认)、内容效率、互动效率、内容量、总播放、视频数、总互动、最近发布;
|
||||||
- UP 卡片显示 UP 头像(无头像时显示首字母占位)、视频数/总时长与最新粉丝数;「总播放」排序时副指标显示「平均播放」(相当于平均播放榜,主副指标对调);UP 详情页也显示头像;
|
- UP 卡片显示 UP 头像(无头像时显示首字母占位)、视频数/总时长与最新粉丝数;「总播放」排序时副指标显示「平均播放」(相当于平均播放榜,主副指标对调);UP 详情页也显示头像;
|
||||||
- 时间筛选:全部 / 近 7 天 / 近 30 天 / 近 90 天 / 近 1 年 / 已入库的年份;不再提供按收藏夹过滤,UP 页也不再按视频数筛选;
|
- 时间筛选:全部 / 近 7 天 / 近 30 天 / 近 90 天 / 近 1 年 / 已入库的年份;不再提供按收藏夹过滤,UP 页也不再按视频数筛选;
|
||||||
- 内容效率 = 播放 / 内容小时;单条视频用 60 分钟先验暴露量向全库均值收缩,只统计时长 ≥ 600 秒、发布满 2 天、播放 ≥ 2000 的视频(综合热度榜同样要求播放 ≥ 2000);UP 内容效率用经验贝叶斯收缩,避免极小样本冲榜;
|
- 内容效率 = 播放 / 内容小时;单条视频用 60 分钟先验暴露量向全库均值收缩,只统计时长 ≥ 600 秒、发布满 2 天、播放 ≥ 2000 的视频(综合热度榜同样要求播放 ≥ 2000);UP 内容效率用经验贝叶斯收缩,避免极小样本冲榜;
|
||||||
|
|||||||
+3
-3
@@ -733,7 +733,7 @@ UP 内容效率 = EB 收缩后的 Σview / Σ内容小时
|
|||||||
- **不用无界年龄项**:reaction 视频通常在发布前两天完成数据积累、两周内接近平台期,`log(age)` 会系统性惩罚老视频,故采用"成熟窗口筛选"而非年龄回归;
|
- **不用无界年龄项**:reaction 视频通常在发布前两天完成数据积累、两周内接近平台期,`log(age)` 会系统性惩罚老视频,故采用"成熟窗口筛选"而非年龄回归;
|
||||||
- `duration` 为空或 ≤ 0(通常只在失效视频出现)不进入效率榜,不插补;
|
- `duration` 为空或 ≤ 0(通常只在失效视频出现)不进入效率榜,不插补;
|
||||||
- UP 效率用 Gamma–Poisson 经验贝叶斯:`rate = (Σview + κ·μ₀) / (Σ内容分钟 + κ)`,`μ₀` 为全库播放/分钟,`κ` 自适应取全库 UP 总内容分钟中位数(配置 `analytics.shrink_minutes` 可覆盖,缺省回退 120 分钟);
|
- UP 效率用 Gamma–Poisson 经验贝叶斯:`rate = (Σview + κ·μ₀) / (Σ内容分钟 + κ)`,`μ₀` 为全库播放/分钟,`κ` 自适应取全库 UP 总内容分钟中位数(配置 `analytics.shrink_minutes` 可覆盖,缺省回退 120 分钟);
|
||||||
- 视频榜默认 Tab 为内容效率,原始播放榜保留;互动单项只保留播放/投币/收藏/评论,点赞、弹幕、转发并入"互动";
|
- 视频榜默认 Tab 为发布时间,原始播放榜保留;互动单项只保留播放/投币/收藏/评论,点赞、弹幕、转发并入"互动";
|
||||||
- **UP 榜的"视频数"与"总时长"并列展示、分别可排序,不合成单一产出分**(两者高度共线,加权求和的权重是任意的)。
|
- **UP 榜的"视频数"与"总时长"并列展示、分别可排序,不合成单一产出分**(两者高度共线,加权求和的权重是任意的)。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -745,8 +745,8 @@ UP 内容效率 = EB 收缩后的 Σview / Σ内容小时
|
|||||||
| 页面 | 内容 |
|
| 页面 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 首页 | 最新动态优先:标题下显示「收录视频 N」;顶部四个数据卡片为「今日入库 / 播放增量 / 7 日粉丝变化 / 数据日期」(前三项可点击跳转);下方为最近发布、增长动态、热度速览三个区块(竖排卡片,移动端每行 2 个、PC 每行 3 个,每处前 6 条);采集/候选等运行期信息只在登录后的「系统」「候选」页可见 |
|
| 首页 | 最新动态优先:标题下显示「收录视频 N」;顶部四个数据卡片为「今日入库 / 播放增量 / 7 日粉丝变化 / 数据日期」(前三项可点击跳转);下方为最近发布、增长动态、热度速览三个区块(竖排卡片,移动端每行 2 个、PC 每行 3 个,每处前 6 条);采集/候选等运行期信息只在登录后的「系统」「候选」页可见 |
|
||||||
| **视频** | 排序下拉:内容效率(默认)/综合热度/发布时间/入库时间/播放/投币/收藏/评论/互动率/播放增量/播放增速;时间下拉:全部/7 天/30 天/90 天/1 年/年份;搜索框约 230px;不再按收藏夹过滤 |
|
| **视频** | 排序下拉:发布时间(默认)/内容效率/综合热度/入库时间/播放/投币/收藏/评论/互动率/播放增量/播放增速;时间下拉:全部/7 天/30 天/90 天/1 年/年份;搜索框约 230px;不再按收藏夹过滤 |
|
||||||
| **UP主** | 独立入口;排序下拉:内容效率/互动效率/内容量/总播放/视频数/平均播放/总互动/最近发布;时间下拉同上;不按视频数筛选 |
|
| **UP主** | 独立入口;排序下拉:平均播放(默认)/内容效率/互动效率/内容量/总播放/视频数/总互动/最近发布;时间下拉同上;不按视频数筛选 |
|
||||||
| 工具条 | 顺序为「排序」(带 label)→ 时间(无 label)→ 右侧自适应搜索框(最大 320px,放大镜按钮内嵌),gap 10px,PC 与移动均一行 |
|
| 工具条 | 顺序为「排序」(带 label)→ 时间(无 label)→ 右侧自适应搜索框(最大 320px,放大镜按钮内嵌),gap 10px,PC 与移动均一行 |
|
||||||
| 数据说明 | 数据来源与更新节奏、三个时间概念、指标公式、时长/成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取当前配置 |
|
| 数据说明 | 数据来源与更新节奏、三个时间概念、指标公式、时长/成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取当前配置 |
|
||||||
| 视频详情 | 元数据 + 分 P 数 + 七项指标 + 内容/互动效率 + 入库时粉丝数 + 单指标趋势 + 同 UP 对比 + 快照表 |
|
| 视频详情 | 元数据 + 分 P 数 + 七项指标 + 内容/互动效率 + 入库时粉丝数 + 单指标趋势 + 同 UP 对比 + 快照表 |
|
||||||
|
|||||||
@@ -861,7 +861,7 @@ class DashboardQueries:
|
|||||||
def list_videos(
|
def list_videos(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
sort: str = "heat",
|
sort: str = "published",
|
||||||
within_days: int | None = None,
|
within_days: int | None = None,
|
||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
growth_days: int | str = "auto",
|
growth_days: int | str = "auto",
|
||||||
@@ -896,7 +896,7 @@ class DashboardQueries:
|
|||||||
def _list_videos_uncached(
|
def _list_videos_uncached(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
sort: str = "heat",
|
sort: str = "published",
|
||||||
within_days: int | None = None,
|
within_days: int | None = None,
|
||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
growth_days: int | str = "auto",
|
growth_days: int | str = "auto",
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}}
|
|||||||
return
|
return
|
||||||
if path == "/api/videos":
|
if path == "/api/videos":
|
||||||
self._send_json(HTTPStatus.OK, queries.list_videos(
|
self._send_json(HTTPStatus.OK, queries.list_videos(
|
||||||
sort=_param(query, "sort", "heat"),
|
sort=_param(query, "sort", "published"),
|
||||||
within_days=_optional_int(query, "within_days"),
|
within_days=_optional_int(query, "within_days"),
|
||||||
year=_year_param(query),
|
year=_year_param(query),
|
||||||
growth_days=_growth_days_param(query),
|
growth_days=_growth_days_param(query),
|
||||||
@@ -403,7 +403,7 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}}
|
|||||||
scope = _param(query, "scope", "video")
|
scope = _param(query, "scope", "video")
|
||||||
if scope == "up":
|
if scope == "up":
|
||||||
self._send_json(HTTPStatus.OK, queries.list_ups(
|
self._send_json(HTTPStatus.OK, queries.list_ups(
|
||||||
sort=_param(query, "sort", "efficiency"),
|
sort=_param(query, "sort", "avg_views"),
|
||||||
within_days=_optional_int(query, "within_days"),
|
within_days=_optional_int(query, "within_days"),
|
||||||
year=_year_param(query),
|
year=_year_param(query),
|
||||||
q=_optional_str(query, "q"),
|
q=_optional_str(query, "q"),
|
||||||
@@ -419,7 +419,7 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}}
|
|||||||
if scope != "video":
|
if scope != "video":
|
||||||
raise ApiError(HTTPStatus.BAD_REQUEST, "bad_request", "scope 必须是 video 或 up")
|
raise ApiError(HTTPStatus.BAD_REQUEST, "bad_request", "scope 必须是 video 或 up")
|
||||||
self._send_json(HTTPStatus.OK, queries.list_videos(
|
self._send_json(HTTPStatus.OK, queries.list_videos(
|
||||||
sort=_param(query, "sort", "heat"),
|
sort=_param(query, "sort", "published"),
|
||||||
within_days=_optional_int(query, "within_days"),
|
within_days=_optional_int(query, "within_days"),
|
||||||
year=_year_param(query),
|
year=_year_param(query),
|
||||||
growth_days=_growth_days_param(query),
|
growth_days=_growth_days_param(query),
|
||||||
@@ -435,7 +435,7 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}}
|
|||||||
return
|
return
|
||||||
if path == "/api/ups":
|
if path == "/api/ups":
|
||||||
self._send_json(HTTPStatus.OK, queries.list_ups(
|
self._send_json(HTTPStatus.OK, queries.list_ups(
|
||||||
sort=_param(query, "sort", "efficiency"),
|
sort=_param(query, "sort", "avg_views"),
|
||||||
within_days=_optional_int(query, "within_days"),
|
within_days=_optional_int(query, "within_days"),
|
||||||
year=_year_param(query),
|
year=_year_param(query),
|
||||||
q=_optional_str(query, "q"),
|
q=_optional_str(query, "q"),
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ function bindControls(root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderScope(root, { query }, scope) {
|
async function renderScope(root, { query }, scope) {
|
||||||
const sort = query.get("sort") || "efficiency";
|
const sort = query.get("sort") || (scope === "up" ? "avg_views" : "published");
|
||||||
const within = query.get("within_days") || "";
|
const within = query.get("within_days") || "";
|
||||||
const year = query.get("year") || "";
|
const year = query.get("year") || "";
|
||||||
const growthDays = query.get("growth_days") || "7";
|
const growthDays = query.get("growth_days") || "7";
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ def test_video_listing_filters_and_sorting():
|
|||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
db, q = setup(tmp)
|
db, q = setup(tmp)
|
||||||
try:
|
try:
|
||||||
|
default = q.list_videos()
|
||||||
|
assert default["sort"] == "published"
|
||||||
|
|
||||||
heat = q.list_videos(sort="heat")
|
heat = q.list_videos(sort="heat")
|
||||||
aids = [item["aid"] for item in heat["items"]]
|
aids = [item["aid"] for item in heat["items"]]
|
||||||
assert 104 not in aids, "removed 视频不应进入榜单"
|
assert 104 not in aids, "removed 视频不应进入榜单"
|
||||||
|
|||||||
@@ -108,11 +108,12 @@ def test_readonly_routes_and_static():
|
|||||||
|
|
||||||
status, _, payload = running.request("/api/videos")
|
status, _, payload = running.request("/api/videos")
|
||||||
assert status == 200
|
assert status == 200
|
||||||
heat_payload = json.loads(payload)["data"]
|
default_payload = json.loads(payload)["data"]
|
||||||
items = heat_payload["items"]
|
items = default_payload["items"]
|
||||||
assert items and items[0]["aid"] == 102
|
assert default_payload["sort"] == "published"
|
||||||
|
assert items and items[0]["aid"] == 103
|
||||||
# 非增长排序:省略 growth_days 时 days/growth_days 回落为 7,不做 7→1
|
# 非增长排序:省略 growth_days 时 days/growth_days 回落为 7,不做 7→1
|
||||||
assert heat_payload["days"] == heat_payload["growth_days"] == 7
|
assert default_payload["days"] == default_payload["growth_days"] == 7
|
||||||
|
|
||||||
status, _, payload = running.json("/api/videos?year=2026")
|
status, _, payload = running.json("/api/videos?year=2026")
|
||||||
assert status == 200
|
assert status == 200
|
||||||
@@ -132,7 +133,14 @@ def test_readonly_routes_and_static():
|
|||||||
|
|
||||||
status, _, payload = running.request("/api/ups")
|
status, _, payload = running.request("/api/ups")
|
||||||
assert status == 200
|
assert status == 200
|
||||||
assert json.loads(payload)["data"]["sort"] == "efficiency"
|
assert json.loads(payload)["data"]["sort"] == "avg_views"
|
||||||
|
|
||||||
|
status, _, payload = running.request("/api/rankings")
|
||||||
|
assert status == 200
|
||||||
|
assert json.loads(payload)["data"]["sort"] == "published"
|
||||||
|
status, _, payload = running.request("/api/rankings?scope=up")
|
||||||
|
assert status == 200
|
||||||
|
assert json.loads(payload)["data"]["sort"] == "avg_views"
|
||||||
|
|
||||||
status, _, payload = running.request("/api/fans?days=7&recent_days=14")
|
status, _, payload = running.request("/api/fans?days=7&recent_days=14")
|
||||||
assert status == 200
|
assert status == 200
|
||||||
|
|||||||
Reference in new issue
Block a user