feat: show drive type on video detail

This commit is contained in:
nianzhibai
2026-05-11 23:37:08 +08:00
parent 4e6f0557f1
commit 7fdb6a0a78
5 changed files with 85 additions and 5 deletions
+2 -2
View File
@@ -10,7 +10,7 @@
- 前台需要登录后访问,支持首页、列表页、搜索、分类/标签筛选、分页、详情播放和相关推荐。
- 视频卡片支持封面、画质、时长、点赞/点踩、移动端点按预览;列表页会记住筛选、分页和滚动位置。
- 播放页提供点赞、标签编辑和 **不再展示**。不再展示是全局隐藏:写入数据库后,该视频不会再出现在首页、列表、相关推荐中,详情接口也会返回 404。
- 播放页会在视频信息中显示来源网盘类型,并提供点赞、标签编辑和 **不再展示**。不再展示是全局隐藏:写入数据库后,该视频不会再出现在首页、列表、相关推荐中,详情接口也会返回 404。
- 管理后台支持网盘管理、视频管理、标签管理和运行时 Teaser 生成开关。
- 视频管理支持按网盘筛选、每页 100 条分页、每个网盘的 Teaser 已生成/待生成/失败统计、单条或全量重生 teaser、编辑标题/作者/分类/标签等元数据。
- 标签管理支持创建标签并自动分类已有视频;内置规则会把常见番号污染归并到 `AV` 等系统标签,降低标签列表噪声。
@@ -129,7 +129,7 @@ OneDrive 当前采用 OpenList 在线 API 的续期方式,不要求用户提
- `/admin/drives`:新增/编辑/删除网盘,触发扫描。
- `/admin/videos`:按网盘查看视频、分页浏览、查看各网盘 Teaser 统计、编辑元数据、重生 teaser。
- `/admin/tags`:新增标签并自动匹配已有视频。
- 播放页“不再展示”是全局隐藏功能当前没有恢复入口,如需恢复可直接把数据库中对应视频的 `hidden` 字段改回 `0`,后续可在管理后台补恢复 UI。
- 播放页:视频信息会显示来源网盘类型;“不再展示”是全局隐藏功能当前没有恢复入口,如需恢复可直接把数据库中对应视频的 `hidden` 字段改回 `0`,后续可在管理后台补恢复 UI。
## 验证
+2 -2
View File
@@ -5,7 +5,7 @@
1. 多家网盘统一抽象(夸克 / 115 / PikPak / 联通沃盘 / OneDrive
2. 视频元数据目录(SQLite+ 扫描 + teaser 预生成
3. REST API(前台)+ 管理后台 + 直链代理
4. 标签池、视频隐藏、按网盘统计和管理查询能力
4. 标签池、视频隐藏、按网盘统计和详情页来源网盘类型展示能力
## 目录
@@ -127,7 +127,7 @@ OneDrive 按 OpenList 默认方式调用 `https://api.oplist.org/onedrive/renewa
- `/admin/drives`:新增、编辑、删除网盘,触发扫描。
- `/admin/videos`:按网盘筛选视频,每页 100 条分页,查看各网盘 Teaser 统计,编辑标题/作者/分类/标签,单条或全量重生 teaser。
- `/admin/tags`:新增标签并用标签规则自动匹配已有视频。
- 播放页提供“不再展示”,点击后会把视频标记为全局隐藏隐藏视频不会再出现在首页、列表、搜索、相关推荐和详情接口中。目前没有管理后台恢复入口,如需恢复可把数据库里对应视频的 `hidden` 字段改回 `0`。
- 播放页视频信息会展示来源网盘类型;同时提供“不再展示”,点击后会把视频标记为全局隐藏隐藏视频不会再出现在首页、列表、搜索、相关推荐和详情接口中。目前没有管理后台恢复入口,如需恢复可把数据库里对应视频的 `hidden` 字段改回 `0`。
## Teaser 生成
+23 -1
View File
@@ -44,6 +44,7 @@ type VideoDTO struct {
Duration string `json:"duration"`
Badges []string `json:"badges"`
Quality string `json:"quality,omitempty"`
SourceLabel string `json:"sourceLabel,omitempty"`
Author string `json:"author"`
Views int `json:"views"`
Favorites int `json:"favorites"`
@@ -157,9 +158,13 @@ func (s *Server) handleVideoDetail(w http.ResponseWriter, r *http.Request) {
related, _, _ := s.Catalog.ListVideos(r.Context(), catalog.ListParams{
Sort: "hot", Page: 1, PageSize: 8,
})
dto := mapVideo(v)
if d, err := s.Catalog.GetDrive(r.Context(), v.DriveID); err == nil {
dto.SourceLabel = driveKindLabel(d.Kind)
}
detail := VideoDetailDTO{
VideoDTO: mapVideo(v),
VideoDTO: dto,
VideoSrc: videoSource(v),
Poster: thumbnailURL(v),
Description: v.Description,
@@ -475,6 +480,23 @@ func needsBrowserTranscode(ext string) bool {
}
}
func driveKindLabel(kind string) string {
switch kind {
case "quark":
return "夸克网盘"
case "p115":
return "115 网盘"
case "pikpak":
return "PikPak"
case "wopan":
return "联通沃盘"
case "onedrive":
return "OneDrive"
default:
return kind
}
}
func buildFFmpegHeaders(h http.Header) string {
if len(h) == 0 {
return ""
+51
View File
@@ -221,6 +221,57 @@ func TestHandleUpdateVideoTagsSavesExistingTags(t *testing.T) {
}
}
func TestHandleVideoDetailIncludesDriveKindLabel(t *testing.T) {
ctx := context.Background()
cat, err := catalog.Open(t.TempDir() + "/catalog.db")
if err != nil {
t.Fatalf("open catalog: %v", err)
}
t.Cleanup(func() {
if err := cat.Close(); err != nil {
t.Fatalf("close catalog: %v", err)
}
})
now := time.Now()
if err := cat.UpsertDrive(ctx, &catalog.Drive{
ID: "drive-onedrive",
Kind: "onedrive",
Name: "Personal Drive",
RootID: "root",
Status: "ok",
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed drive: %v", err)
}
if err := cat.UpsertVideo(ctx, &catalog.Video{
ID: "video-1",
DriveID: "drive-onedrive",
FileID: "file-1",
Title: "Video",
PublishedAt: now,
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed video: %v", err)
}
req := requestWithVideoID(http.MethodGet, "/api/video/video-1", "video-1", strings.NewReader(``))
rr := httptest.NewRecorder()
(&Server{Catalog: cat}).handleVideoDetail(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String())
}
var got VideoDetailDTO
if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
t.Fatalf("decode: %v", err)
}
if got.SourceLabel != "OneDrive" {
t.Fatalf("sourceLabel = %q, want OneDrive", got.SourceLabel)
}
}
func TestHandleHideVideoRemovesVideoFromPublicListAndDetail(t *testing.T) {
ctx := context.Background()
cat, err := catalog.Open(t.TempDir() + "/catalog.db")
+7
View File
@@ -45,6 +45,13 @@ export function VideoInfoPanel({
<span className="info-row__value">{video.publishedAt}</span>
</div>
{video.sourceLabel && (
<div className="info-row">
<span className="info-row__label"></span>
<span className="info-row__value">{video.sourceLabel}</span>
</div>
)}
<div className="info-row">
<span className="info-row__label">/</span>
<div className="info-row__value">