mirror of
https://github.com/nianzhibai/91.git
synced 2026-06-15 08:45:41 +08:00
feat: show drive type on video detail
This commit is contained in:
@@ -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
@@ -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 生成
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user