mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
优化
优化
This commit is contained in:
@@ -32,7 +32,7 @@ func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
role, _ := c.Get(middleware.CtxUserRole)
|
||||
includeHidden := role == "admin" && (c.Query("include_hidden") == "1" || c.Query("all") == "1")
|
||||
includeHidden := role == "admin" && (c.Query("include_hidden") == "1" || c.Query("include_hidden") == "true" || c.Query("all") == "1")
|
||||
if !includeHidden {
|
||||
libs = service.FilterDisplayCloudLibraries(c.Request.Context(), svc.Repo, libs)
|
||||
visibility := mediaVisibilityForRequest(c, svc)
|
||||
@@ -60,7 +60,7 @@ func getLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
role, _ := c.Get(middleware.CtxUserRole)
|
||||
includeHidden := role == "admin" && (c.Query("include_hidden") == "1" || c.Query("all") == "1")
|
||||
includeHidden := role == "admin" && (c.Query("include_hidden") == "1" || c.Query("include_hidden") == "true" || c.Query("all") == "1")
|
||||
if !includeHidden {
|
||||
libs := service.FilterDisplayCloudLibraries(c.Request.Context(), svc.Repo, []model.Library{*lib})
|
||||
if len(libs) == 0 || !service.LibraryVisibleForUser(c.Request.Context(), svc.Repo, libs[0], mediaVisibilityForRequest(c, svc)) {
|
||||
|
||||
@@ -278,18 +278,69 @@ func TestDanmakuSameBase(t *testing.T) {
|
||||
require.False(t, sameDanmakuBase("", "https://api.dandanplay.net"))
|
||||
}
|
||||
|
||||
// fetchCommentWithFallback:配置源与官方同源时不重复请求;
|
||||
// 全失败时带出最后一跳错误。
|
||||
func TestDanmakuFetchCommentWithFallback(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
// fetchCommentWithFallback:配置源与官方同源时不重复请求;
|
||||
// 全失败时带出最后一跳错误。
|
||||
func TestDanmakuFetchCommentWithFallback(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
svc := newDanmakuTestService(t)
|
||||
ctx := context.Background()
|
||||
raw, st, err := svc.fetchCommentWithFallback(ctx, srv.URL, srv.URL, "25484")
|
||||
require.Error(t, err)
|
||||
require.Empty(t, raw)
|
||||
require.Equal(t, "auto", st)
|
||||
}
|
||||
svc := newDanmakuTestService(t)
|
||||
ctx := context.Background()
|
||||
raw, st, err := svc.fetchCommentWithFallback(ctx, srv.URL, srv.URL, "25484")
|
||||
require.Error(t, err)
|
||||
require.Empty(t, raw)
|
||||
require.Equal(t, "auto", st)
|
||||
}
|
||||
|
||||
// 视频即便能命中 Hash 自动识别,当用户传入手动搜索关键词时应跳过 Hash 匹配,走关键词搜索。
|
||||
func TestDanmakuFetchHashMatchSkippedOnManualKeyword(t *testing.T) {
|
||||
videoPath, _ := writeDanmakuTestVideo(t, "测试动画.第01话.mkv")
|
||||
|
||||
// 官方服务同时提供 match 和 search:
|
||||
// match 会返回 episodeId=25484(动画A)
|
||||
// search 会根据关键词返回 episodeId=99999(动画B)
|
||||
mux := http.NewServeMux()
|
||||
var matchCalled bool
|
||||
mux.HandleFunc("/api/v2/match", func(w http.ResponseWriter, r *http.Request) {
|
||||
matchCalled = true
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"success":true,"isMatched":true,"matches":[{"episodeId":25484,"animeId":1001,"animeTitle":"自动识别动画A","episodeTitle":"第1话"}]}`)
|
||||
})
|
||||
mux.HandleFunc("/api/v2/search/episodes", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"hasMore":false,"animes":[{"animeId":2002,"animeTitle":"手动搜索动画B","episodes":[{"episodeId":99999,"episodeTitle":"第1话"}]}]}`)
|
||||
})
|
||||
mux.HandleFunc("/api/v2/comment/25484", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
fmt.Fprint(w, `<?xml version="1.0"?><i><d p="0.5,1,16777215,user1">自动识别弹幕</d></i>`)
|
||||
})
|
||||
mux.HandleFunc("/api/v2/comment/99999", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
fmt.Fprint(w, `<?xml version="1.0"?><i><d p="0.5,1,16777215,user2">手动搜索弹幕</d></i>`)
|
||||
})
|
||||
official := httptest.NewServer(mux)
|
||||
t.Cleanup(official.Close)
|
||||
overrideDanmakuOfficialBase(t, official.URL)
|
||||
|
||||
svc := newDanmakuTestService(t)
|
||||
ctx := context.Background()
|
||||
seedDanmakuVideoMedia(t, svc, "mManual", "自动识别动画A", videoPath, 32000, 1)
|
||||
|
||||
// 1) 默认自动识别:命中 Hash 识别
|
||||
resAuto, err := svc.Fetch(ctx, "mManual", "", "")
|
||||
require.NoError(t, err)
|
||||
require.True(t, matchCalled)
|
||||
require.Equal(t, "hash", resAuto.MatchMode)
|
||||
require.Equal(t, int64(25484), resAuto.EpisodeID)
|
||||
require.Contains(t, resAuto.Raw, "自动识别弹幕")
|
||||
|
||||
// 2) 用户传入手动搜索关键词:跳过 Hash 识别,命中搜索结果动画B
|
||||
resManual, err := svc.Fetch(ctx, "mManual", "手动搜索动画B", "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "search", resManual.MatchMode)
|
||||
require.Equal(t, int64(99999), resManual.EpisodeID)
|
||||
require.Equal(t, "手动搜索动画B", resManual.AnimeTitle)
|
||||
require.Contains(t, resManual.Raw, "手动搜索弹幕")
|
||||
}
|
||||
@@ -219,8 +219,8 @@ func (s *DanmakuService) Fetch(ctx context.Context, mediaID, keyword, episodeID
|
||||
|
||||
target := ""
|
||||
|
||||
// 1) hash 识别:始终走官方 /api/v2/match。
|
||||
if media != nil && media.Path != "" {
|
||||
// 1) hash 识别:始终走官方 /api/v2/match(keyword 手动覆盖时跳过,直接走第 3 层)。
|
||||
if target == "" && !manualKeyword && media != nil && media.Path != "" {
|
||||
if hash, ok := s.mediaHash(ctx, media); ok {
|
||||
fileSize := media.SizeBytes
|
||||
if strings.EqualFold(filepath.Ext(media.Path), ".strm") {
|
||||
|
||||
@@ -123,12 +123,20 @@ func LibraryVisibleForUser(ctx context.Context, repo *repository.Container, lib
|
||||
return false
|
||||
}
|
||||
if repo != nil && repo.DB != nil {
|
||||
var count int64
|
||||
var totalCount int64
|
||||
_ = repo.DB.WithContext(ctx).Model(&model.Media{}).
|
||||
Where("library_id = ? AND nsfw = ?", lib.ID, true).
|
||||
Count(&count).Error
|
||||
if count > 0 {
|
||||
return false
|
||||
Where("library_id = ?", lib.ID).
|
||||
Count(&totalCount).Error
|
||||
if totalCount > 0 {
|
||||
var nsfwCount int64
|
||||
_ = repo.DB.WithContext(ctx).Model(&model.Media{}).
|
||||
Where("library_id = ? AND nsfw = ?", lib.ID, true).
|
||||
Count(&nsfwCount).Error
|
||||
// 仅当整库媒体全部为成人内容(纯成人库)时才隐藏整库;
|
||||
// 含有普通内容的混合媒体库保持库本身可见,具体 NSFW 条目在媒体列表内过滤。
|
||||
if nsfwCount == totalCount {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { libraryAPI } from '../api/library'
|
||||
import { toolsAPI } from '../api/tools'
|
||||
import { openManageLibrariesDialog } from '../components/manageLibrariesDialog'
|
||||
import {
|
||||
LibrariesContent,
|
||||
LibrariesEmptyState,
|
||||
@@ -16,6 +17,32 @@ export function LibrariesPage() {
|
||||
const [repairEpisodeArtwork, setRepairEpisodeArtwork] = useState(false)
|
||||
const [repairMsg, setRepairMsg] = useState('')
|
||||
|
||||
const loadLibraries = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const libs = await libraryAPI.list()
|
||||
const rows = await Promise.all(libs.map(async (library) => {
|
||||
try {
|
||||
if (isSeriesLibraryType(library.type)) {
|
||||
const [seriesPage, mediaPage] = await Promise.all([
|
||||
libraryAPI.listSeries(library.id, 1, 10),
|
||||
libraryAPI.listMedia(library.id, 1, 1, { groupVersions: false }),
|
||||
])
|
||||
return { library, items: [], total: mediaPage.total, cards: seriesPage.items ?? [] } satisfies LibraryPreview
|
||||
}
|
||||
const page = await libraryAPI.listMedia(library.id, 1, 160, { groupVersions: false })
|
||||
const cards = latestLibraryCards(page.items)
|
||||
return { library, items: page.items, total: page.total, cards } satisfies LibraryPreview
|
||||
} catch {
|
||||
return { library, items: [], total: 0, cards: [] } satisfies LibraryPreview
|
||||
}
|
||||
}))
|
||||
setPreviews(rows)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function handleRepairRescrape() {
|
||||
if (repairing) return
|
||||
setRepairing(true)
|
||||
@@ -30,36 +57,14 @@ export function LibrariesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleManageLibraries = async () => {
|
||||
await openManageLibrariesDialog()
|
||||
await loadLibraries()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const libs = await libraryAPI.list()
|
||||
const rows = await Promise.all(libs.map(async (library) => {
|
||||
try {
|
||||
if (isSeriesLibraryType(library.type)) {
|
||||
const [seriesPage, mediaPage] = await Promise.all([
|
||||
libraryAPI.listSeries(library.id, 1, 10),
|
||||
libraryAPI.listMedia(library.id, 1, 1, { groupVersions: false }),
|
||||
])
|
||||
return { library, items: [], total: mediaPage.total, cards: seriesPage.items ?? [] } satisfies LibraryPreview
|
||||
}
|
||||
const page = await libraryAPI.listMedia(library.id, 1, 160, { groupVersions: false })
|
||||
const cards = latestLibraryCards(page.items)
|
||||
return { library, items: page.items, total: page.total, cards } satisfies LibraryPreview
|
||||
} catch {
|
||||
return { library, items: [], total: 0, cards: [] } satisfies LibraryPreview
|
||||
}
|
||||
}))
|
||||
if (!cancelled) setPreviews(rows)
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
loadLibraries().catch(() => undefined)
|
||||
}, [loadLibraries])
|
||||
|
||||
const total = useMemo(() => previews.reduce((sum, preview) => sum + preview.total, 0), [previews])
|
||||
|
||||
@@ -77,6 +82,7 @@ export function LibrariesPage() {
|
||||
repairing={repairing}
|
||||
onRepairEpisodeArtworkChange={setRepairEpisodeArtwork}
|
||||
onRepairRescrape={handleRepairRescrape}
|
||||
onManageLibraries={handleManageLibraries}
|
||||
/>
|
||||
|
||||
{previews.length === 0 ? (
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ArrowRight, Film, FolderOpen, Library as LibraryIcon, Music, PlayCircle
|
||||
import { imageURL } from '../api/client'
|
||||
import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle'
|
||||
import { MediaCard } from '../components/MediaCard'
|
||||
import { openManageLibrariesDialog } from '../components/manageLibrariesDialog'
|
||||
import { seriesCardLink } from '../utils/groupSeries'
|
||||
import { libraryDisplayPath } from './libraryDisplayModel'
|
||||
import { libraryArtworkItems, type LibraryPreview } from './librariesPageModel'
|
||||
@@ -37,6 +36,7 @@ export function LibrariesHeader({
|
||||
repairing,
|
||||
onRepairEpisodeArtworkChange,
|
||||
onRepairRescrape,
|
||||
onManageLibraries,
|
||||
}: {
|
||||
previewCount: number
|
||||
total: number
|
||||
@@ -45,6 +45,7 @@ export function LibrariesHeader({
|
||||
repairing: boolean
|
||||
onRepairEpisodeArtworkChange: (value: boolean) => void
|
||||
onRepairRescrape: () => void
|
||||
onManageLibraries: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-end justify-between gap-4">
|
||||
@@ -72,7 +73,7 @@ export function LibrariesHeader({
|
||||
<RefreshCw size={14} className={repairing ? 'animate-spin' : ''} />
|
||||
{repairing ? '正在启动…' : '全库修复+重刮'}
|
||||
</button>
|
||||
<button type="button" onClick={() => openManageLibrariesDialog()} className="btn-outline">
|
||||
<button type="button" onClick={onManageLibraries} className="btn-outline">
|
||||
管理媒体库
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user