处理bug

This commit is contained in:
truewhile
2026-09-11 15:35:02 +08:00
parent 2bb3610da7
commit e840cbd2c9
20 changed files with 135 additions and 105 deletions
+6 -3
View File
@@ -74,12 +74,15 @@ func localPosterCandidates(mediaPath string) []string {
add(base + "-cover") add(base + "-cover")
add(base + ".cover") add(base + ".cover")
add(base) add(base)
}
for _, name := range []string{"poster", "folder", "cover", "movie", "show"} {
add(name)
}
for _, base := range mediaSidecarBaseVariants(mediaPath) {
add(base + "-thumb") add(base + "-thumb")
add(base + ".thumb") add(base + ".thumb")
} }
for _, name := range []string{"poster", "folder", "cover", "movie", "show", "thumb"} { add("thumb")
add(name)
}
return append(adultArtworkNameCandidates(mediaPath, "poster"), names...) return append(adultArtworkNameCandidates(mediaPath, "poster"), names...)
} }
+5 -1
View File
@@ -84,6 +84,7 @@ func transferSidecarArtwork(srcMedia, dstMedia string, mode TransferMode) error
if dstBase == "" { if dstBase == "" {
dstBase = strings.TrimSuffix(filepath.Base(dstMedia), filepath.Ext(dstMedia)) dstBase = strings.TrimSuffix(filepath.Base(dstMedia), filepath.Ext(dstMedia))
} }
srcAdultCode := AdultCodeFromMediaPath(srcMedia)
for _, src := range sources { for _, src := range sources {
name := filepath.Base(src) name := filepath.Base(src)
// Remap any legacy "Title.mkv-poster.jpg" onto the shared destination stem. // Remap any legacy "Title.mkv-poster.jpg" onto the shared destination stem.
@@ -97,7 +98,10 @@ func transferSidecarArtwork(srcMedia, dstMedia string, mode TransferMode) error
} }
} }
dstName := name dstName := name
if suffix != "" && dstBase != "" { if srcAdultCode != "" && strings.HasPrefix(strings.ToLower(name), strings.ToLower(srcAdultCode)) {
// Adult sidecars are keyed by code (e.g. ADN-188-poster.jpg) and should
// follow the media without being renamed to the display title stem.
} else if suffix != "" && dstBase != "" {
dstName = dstBase + suffix dstName = dstBase + suffix
} }
dst := filepath.Join(dstDir, dstName) dst := filepath.Join(dstDir, dstName)
@@ -46,14 +46,21 @@ func (s *ScraperService) writeMediaArtworkFilesAfterScrape(ctx context.Context,
} }
isAdult := shouldCropAdultPoster(refreshed, lib) isAdult := shouldCropAdultPoster(refreshed, lib)
artworkUpdates := map[string]any{} artworkUpdates := map[string]any{}
shouldPersistLocalArtworkURL := func(raw string) bool {
return isAdult || !isHTTPish(raw)
}
if refreshed.PosterURL != "" { if refreshed.PosterURL != "" {
if dst := s.downloadArtworkToPathWithOptions(ctx, dir, base+"-poster", refreshed.PosterURL, isAdult); dst != "" { if dst := s.downloadArtworkToPathWithOptions(ctx, dir, base+"-poster", refreshed.PosterURL, isAdult); dst != "" {
artworkUpdates["poster_url"] = filepath.Join(filepath.Dir(refreshed.Path), filepath.Base(dst)) if shouldPersistLocalArtworkURL(refreshed.PosterURL) {
artworkUpdates["poster_url"] = filepath.Join(filepath.Dir(refreshed.Path), filepath.Base(dst))
}
} }
} }
if refreshed.BackdropURL != "" { if refreshed.BackdropURL != "" {
if dst := s.downloadArtworkToPathWithOptions(ctx, dir, base+"-backdrop", refreshed.BackdropURL, false); dst != "" { if dst := s.downloadArtworkToPathWithOptions(ctx, dir, base+"-backdrop", refreshed.BackdropURL, false); dst != "" {
artworkUpdates["backdrop_url"] = filepath.Join(filepath.Dir(refreshed.Path), filepath.Base(dst)) if shouldPersistLocalArtworkURL(refreshed.BackdropURL) {
artworkUpdates["backdrop_url"] = filepath.Join(filepath.Dir(refreshed.Path), filepath.Base(dst))
}
} }
} else if isAdult && refreshed.PosterURL != "" { } else if isAdult && refreshed.PosterURL != "" {
// 番号海报原图为完整封套横图,在无独立背景图时直接作为背景图写出 // 番号海报原图为完整封套横图,在无独立背景图时直接作为背景图写出
+4
View File
@@ -825,6 +825,7 @@ func TestWalkRemoteConcurrent(t *testing.T) {
if err := st.walkRemote(); err != nil { if err := st.walkRemote(); err != nil {
t.Fatalf("walkRemote failed: %v", err) t.Fatalf("walkRemote failed: %v", err)
} }
st.flushPreferredVideos()
for _, dir := range []string{"root", "a", "a1", "b"} { for _, dir := range []string{"root", "a", "a1", "b"} {
if provider.listed[dir] != 1 { if provider.listed[dir] != 1 {
@@ -1113,6 +1114,7 @@ func TestWalk115FlatConcurrentProcessing(t *testing.T) {
if err := st.walk115Flat(oc); err != nil { if err := st.walk115Flat(oc); err != nil {
t.Fatalf("walk115Flat: %v", err) t.Fatalf("walk115Flat: %v", err)
} }
st.flushPreferredVideos()
// 3 个视频的 strm 全部生成 // 3 个视频的 strm 全部生成
if st.rec.NewStrm != 3 { if st.rec.NewStrm != 3 {
@@ -1168,6 +1170,7 @@ func TestWalk115FlatConcurrentProcessing(t *testing.T) {
if err := st2.walk115Flat(oc); err != nil { if err := st2.walk115Flat(oc); err != nil {
t.Fatalf("incremental walk115Flat: %v", err) t.Fatalf("incremental walk115Flat: %v", err)
} }
st2.flushPreferredVideos()
if infoCalls != 0 { if infoCalls != 0 {
t.Fatalf("incremental sync should reuse dir cache, got %d get_info calls", infoCalls) t.Fatalf("incremental sync should reuse dir cache, got %d get_info calls", infoCalls)
} }
@@ -1461,6 +1464,7 @@ func TestWalk115AdaptiveHierarchicalFlatScan(t *testing.T) {
if err := st.walk115Flat(oc); err != nil { if err := st.walk115Flat(oc); err != nil {
t.Fatalf("walk115Flat adaptive failed: %v", err) t.Fatalf("walk115Flat adaptive failed: %v", err)
} }
st.flushPreferredVideos()
// 1个根目录视频 + 2个Heyzo视频 + 1个S1视频 = 共4个视频成功生成 .strm // 1个根目录视频 + 2个Heyzo视频 + 1个S1视频 = 共4个视频成功生成 .strm
if st.rec.NewStrm != 4 { if st.rec.NewStrm != 4 {
+3 -1
View File
@@ -55,7 +55,9 @@ export function LayoutUserMenu({
const [menuPosition, setMenuPosition] = useState<MenuPosition | null>(null) const [menuPosition, setMenuPosition] = useState<MenuPosition | null>(null)
const [isOtpOpen, setIsOtpOpen] = useState(false) const [isOtpOpen, setIsOtpOpen] = useState(false)
onCloseRef.current = onClose useEffect(() => {
onCloseRef.current = onClose
}, [onClose])
const updateMenuPosition = useCallback(() => { const updateMenuPosition = useCallback(() => {
const trigger = triggerRef.current const trigger = triggerRef.current
@@ -0,0 +1,34 @@
import { X } from 'lucide-react'
import { AdminLibraryPanel } from '../pages/AdminLibraryPanel'
export function ManageLibrariesDialogView({ onClose }: { onClose: () => void }) {
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/35 p-2 sm:p-4 backdrop-blur-sm"
onClick={onClose}
>
<div
role="dialog"
aria-modal="true"
className="flex h-[calc(100dvh-1rem)] sm:h-[85vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl sm:rounded-3xl border border-white/70 bg-white shadow-2xl"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-center justify-between border-b border-gray-100 px-4 py-3 sm:px-6 sm:py-4">
<h3 className="font-display text-base sm:text-lg font-bold text-ink-600">管理媒体库</h3>
<button
type="button"
onClick={onClose}
className="rounded-xl p-1.5 text-ink-50 transition hover:bg-gray-100 hover:text-ink-600"
title="关闭"
>
<X size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-3 sm:p-6">
<AdminLibraryPanel />
</div>
</div>
</div>
)
}
+7 -8
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { import {
Captions, Captions,
CaptionsOff, CaptionsOff,
@@ -84,11 +84,11 @@ export function PlayerControls({
streamOffset = 0, streamOffset = 0,
onSeekAbsolute, onSeekAbsolute,
}: PlayerControlsProps) { }: PlayerControlsProps) {
const video = () => videoRef.current const video = useCallback(() => videoRef.current, [videoRef])
const container = () => const container = useCallback(() =>
videoRef.current?.closest<HTMLElement>('[data-player-stage]') ?? videoRef.current?.closest<HTMLElement>('[data-player-stage]') ??
videoRef.current?.parentElement ?? videoRef.current?.parentElement ??
null null, [videoRef])
const [playing, setPlaying] = useState(false) const [playing, setPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0) const [currentTime, setCurrentTime] = useState(0)
@@ -255,15 +255,14 @@ export function PlayerControls({
el.removeEventListener('leavepictureinpicture', syncPip) el.removeEventListener('leavepictureinpicture', syncPip)
if (hideTimerRef.current) clearTimeout(hideTimerRef.current) if (hideTimerRef.current) clearTimeout(hideTimerRef.current)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [container, video, knownDuration, streamOffset, onUiVisibleChange])
}, [videoRef, knownDuration, streamOffset, onUiVisibleChange])
// Keep the scrubber max in sync when metadata duration arrives after mount. // Keep the scrubber max in sync when metadata duration arrives after mount.
useEffect(() => { useEffect(() => {
const el = video() const el = video()
const streamDur = el && Number.isFinite(el.duration) ? el.duration : 0 const streamDur = el && Number.isFinite(el.duration) ? el.duration : 0
setDuration(Math.max(knownDuration || 0, streamOffset + streamDur)) setDuration(Math.max(knownDuration || 0, streamOffset + streamDur))
}, [knownDuration, streamOffset]) }, [knownDuration, streamOffset, video])
// 当悬停或菜单状态改变时,更新控制栏计时器 // 当悬停或菜单状态改变时,更新控制栏计时器
useEffect(() => { useEffect(() => {
@@ -277,7 +276,7 @@ export function PlayerControls({
hideTimerRef.current = setTimeout(() => onUiVisibleChange(false), 3000) hideTimerRef.current = setTimeout(() => onUiVisibleChange(false), 3000)
} }
} }
}, [controlsHovered, isScrubbing, subtitleMenuOpen, danmakuOpen, playlistOpen, onUiVisibleChange]) }, [controlsHovered, isScrubbing, subtitleMenuOpen, danmakuOpen, playlistOpen, onUiVisibleChange, video])
const togglePlay = () => { const togglePlay = () => {
const el = video() const el = video()
+2 -34
View File
@@ -1,7 +1,6 @@
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { X } from 'lucide-react'
import { AdminLibraryPanel } from '../pages/AdminLibraryPanel' import { ManageLibrariesDialogView } from './ManageLibrariesDialogView'
export function openManageLibrariesDialog(): Promise<void> { export function openManageLibrariesDialog(): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -15,37 +14,6 @@ export function openManageLibrariesDialog(): Promise<void> {
resolve() resolve()
} }
root.render(<ManageLibrariesDialog onClose={close} />) root.render(<ManageLibrariesDialogView onClose={close} />)
}) })
} }
function ManageLibrariesDialog({ onClose }: { onClose: () => void }) {
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/35 p-2 sm:p-4 backdrop-blur-sm"
onClick={onClose}
>
<div
role="dialog"
aria-modal="true"
className="flex h-[calc(100dvh-1rem)] sm:h-[85vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl sm:rounded-3xl border border-white/70 bg-white shadow-2xl"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-center justify-between border-b border-gray-100 px-4 py-3 sm:px-6 sm:py-4">
<h3 className="font-display text-base sm:text-lg font-bold text-ink-600">管理媒体库</h3>
<button
type="button"
onClick={onClose}
className="rounded-xl p-1.5 text-ink-50 transition hover:bg-gray-100 hover:text-ink-600"
title="关闭"
>
<X size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-3 sm:p-6">
<AdminLibraryPanel />
</div>
</div>
</div>
)
}
+4 -1
View File
@@ -15,7 +15,10 @@ export function useWebSocket(onEvent: (topic: string, payload: unknown) => void)
const ref = useRef<WebSocket | null>(null) const ref = useRef<WebSocket | null>(null)
const token = useAuthStore((s) => s.token) const token = useAuthStore((s) => s.token)
const onEventRef = useRef(onEvent) const onEventRef = useRef(onEvent)
onEventRef.current = onEvent
useEffect(() => {
onEventRef.current = onEvent
}, [onEvent])
useEffect(() => { useEffect(() => {
if (!token) return if (!token) return
+6 -6
View File
@@ -28,7 +28,7 @@ export function AdminLibraryTable({ libs, ...actions }: LibraryTableProps) {
const [browsingRoot, setBrowsingRoot] = useState<{ libraryID: string; root: LibraryRoot; initialPath?: string } | null>(null) const [browsingRoot, setBrowsingRoot] = useState<{ libraryID: string; root: LibraryRoot; initialPath?: string } | null>(null)
const [addingRootLib, setAddingRootLib] = useState<Library | null>(null) const [addingRootLib, setAddingRootLib] = useState<Library | null>(null)
const [draggingId, setDraggingId] = useState<string | null>(null) const [draggingId, setDraggingId] = useState<string | null>(null)
const dragOverId = useRef<string | null>(null) const [dragOverId, setDragOverId] = useState<string | null>(null)
const handleSelectRootPath = (selectedPath: string) => { const handleSelectRootPath = (selectedPath: string) => {
if (browsingRoot) { if (browsingRoot) {
@@ -59,7 +59,7 @@ export function AdminLibraryTable({ libs, ...actions }: LibraryTableProps) {
const handleDrop = (e: DragEvent, overId: string) => { const handleDrop = (e: DragEvent, overId: string) => {
e.preventDefault() e.preventDefault()
dragOverId.current = null setDragOverId(null)
setDraggingId(null) setDraggingId(null)
if (draggingId && overId !== draggingId) { if (draggingId && overId !== draggingId) {
handleReorder(draggingId, overId) handleReorder(draggingId, overId)
@@ -90,19 +90,19 @@ export function AdminLibraryTable({ libs, ...actions }: LibraryTableProps) {
onBrowseRoot={(root) => setBrowsingRoot({ libraryID: library.id, root, initialPath: root.path })} onBrowseRoot={(root) => setBrowsingRoot({ libraryID: library.id, root, initialPath: root.path })}
onOpenAddRoot={() => setAddingRootLib(library)} onOpenAddRoot={() => setAddingRootLib(library)}
dragging={draggingId === library.id} dragging={draggingId === library.id}
dragOver={dragOverId.current === library.id} dragOver={dragOverId === library.id}
onDragStart={(e) => { onDragStart={(e) => {
e.dataTransfer.effectAllowed = 'move' e.dataTransfer.effectAllowed = 'move'
dragOverId.current = null setDragOverId(null)
setDraggingId(library.id) setDraggingId(library.id)
}} }}
onDragOver={(e) => { onDragOver={(e) => {
e.preventDefault() e.preventDefault()
e.dataTransfer.dropEffect = 'move' e.dataTransfer.dropEffect = 'move'
if (dragOverId.current !== library.id) dragOverId.current = library.id if (dragOverId !== library.id) setDragOverId(library.id)
}} }}
onDragEnd={() => { onDragEnd={() => {
dragOverId.current = null setDragOverId(null)
setDraggingId(null) setDraggingId(null)
}} }}
onDrop={(e) => handleDrop(e, library.id)} onDrop={(e) => handleDrop(e, library.id)}
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { import {
+2 -2
View File
@@ -3,7 +3,7 @@ import { Database, HardDrive, PieChart, RefreshCw } from 'lucide-react'
import { storageAPI, type StorageBreakdown } from '../api/storage' import { storageAPI, type StorageBreakdown } from '../api/storage'
export function fmtBytes(n: number): string { function fmtBytes(n: number): string {
if (!n) return '0 B' if (!n) return '0 B'
const u = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] const u = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
let v = n let v = n
@@ -15,7 +15,7 @@ export function fmtBytes(n: number): string {
return `${v.toFixed(2)} ${u[i]}` return `${v.toFixed(2)} ${u[i]}`
} }
export function fmtHours(seconds: number): string { function fmtHours(seconds: number): string {
if (!seconds) return '—' if (!seconds) return '—'
const h = Math.floor(seconds / 3600) const h = Math.floor(seconds / 3600)
return `${h.toLocaleString()} h` return `${h.toLocaleString()} h`
+8 -3
View File
@@ -311,7 +311,10 @@ export function PlayerPage() {
// Chromium 会把被中断的首次加载报告成播放错误并误触发 HLS 回退。 // Chromium 会把被中断的首次加载报告成播放错误并误触发 HLS 回退。
const activeBurnedSubtitleStream = mode === 'hls' ? burnedSubtitleStream : undefined const activeBurnedSubtitleStream = mode === 'hls' ? burnedSubtitleStream : undefined
const mediaRef = useRef(media) const mediaRef = useRef(media)
mediaRef.current = media useEffect(() => {
mediaRef.current = media
}, [media])
useEffect(() => { useEffect(() => {
if (!mediaId || !ref.current) return if (!mediaId || !ref.current) return
const currentMedia = mediaRef.current const currentMedia = mediaRef.current
@@ -498,9 +501,11 @@ export function PlayerPage() {
// 使用 ref 实时同步进度计算所需的状态,避免每次 hlsStartSec 改变都触发 cleanup 并误上报旧进度 // 使用 ref 实时同步进度计算所需的状态,避免每次 hlsStartSec 改变都触发 cleanup 并误上报旧进度
const hlsStartSecRef = useRef(hlsStartSec) const hlsStartSecRef = useRef(hlsStartSec)
hlsStartSecRef.current = hlsStartSec
const modeRef = useRef(mode) const modeRef = useRef(mode)
modeRef.current = mode useEffect(() => {
hlsStartSecRef.current = hlsStartSec
modeRef.current = mode
}, [hlsStartSec, mode])
// Persist resume position every 10 seconds while playing, and immediately upon pause/unmount. // Persist resume position every 10 seconds while playing, and immediately upon pause/unmount.
useEffect(() => { useEffect(() => {
+4 -4
View File
@@ -23,7 +23,8 @@ import { imageURL } from '../api/client'
import { scraperAPI } from '../api/scraper' import { scraperAPI } from '../api/scraper'
import type { ScrapeQueueSnapshot, ScrapeTask, ScrapeTaskStatus } from '../types/scraper' import type { ScrapeQueueSnapshot, ScrapeTask, ScrapeTaskStatus } from '../types/scraper'
import { apiErrorMessage, formatTime, taskStatusMeta } from './StrmManagePage' import { apiErrorMessage, formatTime, taskStatusMeta } from './StrmManagePage'
import { PROVIDER_LABELS, ScrapeDetailModal } from './scraper-queue/ScrapeDetailModal' import { ScrapeDetailModal } from './scraper-queue/ScrapeDetailModal'
import { PROVIDER_LABELS } from './scraper-queue/scrapeLabels'
import { copyToClipboard, useTaskSelection } from './queue-shared' import { copyToClipboard, useTaskSelection } from './queue-shared'
const FILTERS: { key: 'all' | ScrapeTaskStatus; label: string; icon: typeof Clock; color: string }[] = [ const FILTERS: { key: 'all' | ScrapeTaskStatus; label: string; icon: typeof Clock; color: string }[] = [
@@ -196,9 +197,8 @@ export function ScraperQueuePage({ embedded = false }: { embedded?: boolean }) {
} }
// Filter and search // Filter and search
const tasks = snapshot?.tasks ?? []
const filteredTasks = useMemo(() => { const filteredTasks = useMemo(() => {
let list = tasks let list = snapshot?.tasks ?? []
if (filter !== 'all') { if (filter !== 'all') {
list = list.filter((t) => t.status === filter) list = list.filter((t) => t.status === filter)
} }
@@ -214,7 +214,7 @@ export function ScraperQueuePage({ embedded = false }: { embedded?: boolean }) {
) )
} }
return list return list
}, [tasks, filter, search]) }, [snapshot?.tasks, filter, search])
const counts = snapshot?.counts const counts = snapshot?.counts
const pendingCount = counts?.pending ?? 0 const pendingCount = counts?.pending ?? 0
+4 -4
View File
@@ -21,7 +21,8 @@ import { strmAPI } from '../api/strm'
import type { StrmQueueSnapshot, StrmTask, StrmTaskStatus } from '../types/strm' import type { StrmQueueSnapshot, StrmTask, StrmTaskStatus } from '../types/strm'
import { STRM_PROVIDER_LABELS } from '../types/strm' import { STRM_PROVIDER_LABELS } from '../types/strm'
import { apiErrorMessage, formatBytes, formatTime, taskStatusMeta } from './StrmManagePage' import { apiErrorMessage, formatBytes, formatTime, taskStatusMeta } from './StrmManagePage'
import { TaskDetailModal, getFileIcon } from './strm-queue/TaskDetailModal' import { TaskDetailModal } from './strm-queue/TaskDetailModal'
import { getFileIcon } from './strm-queue/taskFileIcon'
import { copyToClipboard, useTaskSelection } from './queue-shared' import { copyToClipboard, useTaskSelection } from './queue-shared'
const FILTERS: { key: 'all' | StrmTaskStatus; label: string; icon: typeof Clock; color: string }[] = [ const FILTERS: { key: 'all' | StrmTaskStatus; label: string; icon: typeof Clock; color: string }[] = [
@@ -190,9 +191,8 @@ export function StrmQueuePanel({
} }
// Filter and search tasks in memory // Filter and search tasks in memory
const tasks = snapshot?.tasks ?? []
const filteredTasks = useMemo(() => { const filteredTasks = useMemo(() => {
let list = tasks let list = snapshot?.tasks ?? []
if (filter !== 'all') { if (filter !== 'all') {
list = list.filter((t) => t.status === filter) list = list.filter((t) => t.status === filter)
} }
@@ -207,7 +207,7 @@ export function StrmQueuePanel({
) )
} }
return list return list
}, [tasks, filter, search]) }, [snapshot?.tasks, filter, search])
const counts = snapshot?.counts const counts = snapshot?.counts
const pendingCount = counts?.pending ?? 0 const pendingCount = counts?.pending ?? 0
@@ -4,21 +4,7 @@ import { AlertCircle, Ban, Copy, ExternalLink, RefreshCw, Sparkles, Trash2, X }
import { imageURL } from '../../api/client' import { imageURL } from '../../api/client'
import type { ScrapeTask } from '../../types/scraper' import type { ScrapeTask } from '../../types/scraper'
import { formatTime, taskStatusMeta } from '../StrmManagePage' import { formatTime, taskStatusMeta } from '../StrmManagePage'
import { PROVIDER_LABELS, TYPE_LABELS } from './scrapeLabels'
export const PROVIDER_LABELS: Record<string, string> = {
tmdb: 'TheMovieDB',
douban: '豆瓣 Douban',
bangumi: 'Bangumi 番组计划',
thetvdb: 'TheTVDB',
metatube: 'MetaTube',
}
export const TYPE_LABELS: Record<string, string> = {
movie: '电影',
tv: '剧集',
anime: '动漫',
adult: 'Adult',
}
export function ScrapeDetailModal({ export function ScrapeDetailModal({
task, task,
@@ -0,0 +1,14 @@
export const PROVIDER_LABELS: Record<string, string> = {
tmdb: 'TheMovieDB',
douban: '豆瓣 Douban',
bangumi: 'Bangumi 番组计划',
thetvdb: 'TheTVDB',
metatube: 'MetaTube',
}
export const TYPE_LABELS: Record<string, string> = {
movie: '电影',
tv: '剧集',
anime: '动漫',
adult: 'Adult',
}
+2 -19
View File
@@ -1,26 +1,9 @@
import type { ReactNode } from 'react' import { AlertCircle, Ban, Copy, RefreshCw, Trash2, X } from 'lucide-react'
import { AlertCircle, Ban, Copy, FileText, Film, Image as ImageIcon, File, MessageSquare, RefreshCw, Trash2, X } from 'lucide-react'
import type { StrmTask } from '../../types/strm' import type { StrmTask } from '../../types/strm'
import { STRM_PROVIDER_LABELS } from '../../types/strm' import { STRM_PROVIDER_LABELS } from '../../types/strm'
import { formatBytes, formatTime, taskStatusMeta } from '../StrmManagePage' import { formatBytes, formatTime, taskStatusMeta } from '../StrmManagePage'
import { getFileIcon } from './taskFileIcon'
export function getFileIcon(filename: string): ReactNode {
const ext = filename.split('.').pop()?.toLowerCase() ?? ''
if (['nfo', 'txt', 'xml', 'json'].includes(ext)) {
return <FileText size={15} className="text-amber-500 shrink-0" />
}
if (['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'svg'].includes(ext)) {
return <ImageIcon size={15} className="text-blue-500 shrink-0" />
}
if (['srt', 'ass', 'ssa', 'sub', 'vtt'].includes(ext)) {
return <MessageSquare size={15} className="text-purple-500 shrink-0" />
}
if (['mkv', 'mp4', 'avi', 'mov', 'wmv', 'ts', 'flv', 'iso', 'm4v', 'strm'].includes(ext)) {
return <Film size={15} className="text-emerald-500 shrink-0" />
}
return <File size={15} className="text-gray-400 shrink-0" />
}
export function TaskDetailModal({ export function TaskDetailModal({
task, task,
+19
View File
@@ -0,0 +1,19 @@
import type { ReactNode } from 'react'
import { File, FileText, Film, Image as ImageIcon, MessageSquare } from 'lucide-react'
export function getFileIcon(filename: string): ReactNode {
const ext = filename.split('.').pop()?.toLowerCase() ?? ''
if (['nfo', 'txt', 'xml', 'json'].includes(ext)) {
return <FileText size={15} className="text-amber-500 shrink-0" />
}
if (['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'svg'].includes(ext)) {
return <ImageIcon size={15} className="text-blue-500 shrink-0" />
}
if (['srt', 'ass', 'ssa', 'sub', 'vtt'].includes(ext)) {
return <MessageSquare size={15} className="text-purple-500 shrink-0" />
}
if (['mkv', 'mp4', 'avi', 'mov', 'wmv', 'ts', 'flv', 'iso', 'm4v', 'strm'].includes(ext)) {
return <Film size={15} className="text-emerald-500 shrink-0" />
}
return <File size={15} className="text-gray-400 shrink-0" />
}
+1 -1
View File
@@ -180,7 +180,7 @@ function useLibraryActions(refresh: () => Promise<void>) {
} }
const toggleCarouselLibrary = async (library: Library) => { const toggleCarouselLibrary = async (library: Library) => {
const next = !Boolean(library.carousel_enabled) const next = !library.carousel_enabled
await libraryAPI.update(library.id, { carousel_enabled: next }) await libraryAPI.update(library.id, { carousel_enabled: next })
toast.success(next ? `「${library.name}」已加入首页轮播` : `「${library.name}」已移出首页轮播`) toast.success(next ? `「${library.name}」已加入首页轮播` : `「${library.name}」已移出首页轮播`)
await refresh() await refresh()