diff --git a/internal/handler/media.go b/internal/handler/media.go index cbed24d..2dcb94b 100644 --- a/internal/handler/media.go +++ b/internal/handler/media.go @@ -16,12 +16,13 @@ import ( ) type createLibraryReq struct { - Name string `json:"name" binding:"required"` - Path string `json:"path"` - Paths []string `json:"paths"` - Roots []service.LibraryRootInput `json:"roots"` - Type string `json:"type"` - CoverURL string `json:"cover_url"` + Name string `json:"name"` + Path string `json:"path"` + Paths []string `json:"paths"` + Roots []service.LibraryRootInput `json:"roots"` + Type string `json:"type"` + CoverURL string `json:"cover_url"` + CreatePerSubfolder bool `json:"create_per_subfolder"` } func listLibrariesHandler(svc *service.Container) gin.HandlerFunc { @@ -88,9 +89,38 @@ func createLibraryHandler(svc *service.Container) gin.HandlerFunc { } } if len(roots) == 0 && strings.TrimSpace(req.Path) != "" { - roots = append(roots, service.LibraryRootInput{Path: req.Path}) +roots = append(roots, service.LibraryRootInput{Path: req.Path}) + } + var l *model.Library + if req.CreatePerSubfolder { + parent := "" + if len(roots) > 0 { + parent = roots[0].Path + } else if strings.TrimSpace(req.Path) != "" { + parent = req.Path } - l, err := svc.Media.CreateLibraryWithRootsAndCover(c.Request.Context(), req.Name, req.Type, req.CoverURL, roots) + created, err := svc.Media.CreateLibrariesPerSubfolder(c.Request.Context(), parent, req.Type, req.CoverURL) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + uid, _ := c.Get("ctx_user_id") + for i := range created { + lib := &created[i] + svc.Audit.Record(c.Request.Context(), toString(uid), "library.create", lib.ID, c.ClientIP(), lib.Path) + if svc.Watcher != nil { + go func() { _ = svc.Watcher.Refresh(context.Background()) }() + } + for _, root := range lib.Roots { + if root.Enabled { + queueLibraryRootScan(svc, lib.ID, root.ID) + } + } + } + c.JSON(http.StatusCreated, gin.H{"libraries": created}) + return + } + l, err := svc.Media.CreateLibraryWithRootsAndCover(c.Request.Context(), req.Name, req.Type, req.CoverURL, roots) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/internal/service/media_library_roots.go b/internal/service/media_library_roots.go index 014cb10..52c9a0f 100644 --- a/internal/service/media_library_roots.go +++ b/internal/service/media_library_roots.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "path/filepath" "strings" @@ -59,6 +60,46 @@ func (s *MediaService) CreateLibraryWithRootsAndCover(ctx context.Context, name, return lib, nil } +// CreateLibrariesPerSubfolder 为 parent 目录下的每个直接子目录各建一个媒体库, +// 媒体库名取子目录名,路径指向该子目录。kind 为空时按子目录名推断类型。 +func (s *MediaService) CreateLibrariesPerSubfolder(ctx context.Context, parent, kind, coverURL string) ([]model.Library, error) { + parent = strings.TrimSpace(parent) + if parent == "" { + return nil, errors.New("parent path required") + } + dir, err := resolveAccessibleLibraryPath(parent) + if err != nil { + return nil, err + } + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("read directory failed: %w", err) + } + subdirs := make([]string, 0, len(entries)) + for _, entry := range entries { + if !entry.IsDir() { + continue + } + if strings.HasPrefix(entry.Name(), ".") { + continue + } + subdirs = append(subdirs, filepath.Join(dir, entry.Name())) + } + if len(subdirs) == 0 { + return nil, errors.New("no subfolders found") + } + created := make([]model.Library, 0, len(subdirs)) + for _, subdir := range subdirs { + name := filepath.Base(subdir) + lib, err := s.CreateLibraryWithRootsAndCover(ctx, name, kind, coverURL, []LibraryRootInput{{Path: subdir}}) + if err != nil { + return nil, fmt.Errorf("create library for %s: %w", subdir, err) + } + created = append(created, *lib) + } + return created, nil +} + func (s *MediaService) UpdateLibraryCover(ctx context.Context, libraryID, coverURL string) error { return s.repo.DB.WithContext(ctx).Model(&model.Library{}).Where("id = ?", libraryID). Update("cover_url", strings.TrimSpace(coverURL)).Error diff --git a/web/src/api/library.ts b/web/src/api/library.ts index 1e46a1e..4348650 100644 --- a/web/src/api/library.ts +++ b/web/src/api/library.ts @@ -102,6 +102,9 @@ export const libraryAPI = { createWithRoots: (name: string, type: string, roots: LibraryRootInput[], coverURL = '') => api.post('/libraries', { name, type, roots, cover_url: coverURL }).then((r) => r.data), + createPerSubfolder: (parentPath: string, type: string, coverURL = '') => + api.post<{ libraries: Library[] }>('/libraries', { path: parentPath, type, cover_url: coverURL, create_per_subfolder: true }).then((r) => r.data), + update: (id: string, payload: { cover_url: string }) => api.patch(`/libraries/${id}`, payload).then((r) => r.data), diff --git a/web/src/pages/AdminLibraryPanel.tsx b/web/src/pages/AdminLibraryPanel.tsx index 195b68b..ab8d649 100644 --- a/web/src/pages/AdminLibraryPanel.tsx +++ b/web/src/pages/AdminLibraryPanel.tsx @@ -13,9 +13,11 @@ export function AdminLibraryPanel() { type={createForm.type} coverURL={createForm.coverURL} roots={createForm.roots} + createPerSubfolder={createForm.createPerSubfolder} onNameChange={createForm.setName} onTypeChange={createForm.setType} onCoverURLChange={createForm.setCoverURL} + onCreatePerSubfolderChange={createForm.setCreatePerSubfolder} onRootChange={createForm.updateRoot} onAddRoot={createForm.addRoot} onRemoveRoot={createForm.removeRoot} diff --git a/web/src/pages/AdminLibraryPanelSections.tsx b/web/src/pages/AdminLibraryPanelSections.tsx index 1833c3d..b9b939d 100644 --- a/web/src/pages/AdminLibraryPanelSections.tsx +++ b/web/src/pages/AdminLibraryPanelSections.tsx @@ -9,9 +9,11 @@ type CreateFormProps = { type: string coverURL: string roots: RootDraft[] + createPerSubfolder: boolean onNameChange: (value: string) => void onTypeChange: (value: string) => void onCoverURLChange: (value: string) => void + onCreatePerSubfolderChange: (value: boolean) => void onRootChange: (index: number, patch: Partial) => void onAddRoot: () => void onRemoveRoot: (index: number) => void @@ -23,9 +25,11 @@ export function AdminLibraryCreateForm({ type, coverURL, roots, + createPerSubfolder, onNameChange, onTypeChange, onCoverURLChange, + onCreatePerSubfolderChange, onRootChange, onAddRoot, onRemoveRoot, @@ -50,9 +54,9 @@ export function AdminLibraryCreateForm({ <>
onNameChange(e.target.value)} /> @@ -81,15 +85,31 @@ export function AdminLibraryCreateForm({ onRemove={onRemoveRoot} /> ))} - + {!createPerSubfolder && ( + + )}

支持直接点选或手动输入;名称和类型与现有媒体库一致时,会自动把这里填写的路径追加到该媒体库。

+ + {createPerSubfolder && ( +

+ 批处理模式:仅取上方第一个路径作为父级目录,会为其中每个子文件夹分别创建媒体库,可自选类型用于整体推断。 +

+ )}
diff --git a/web/src/pages/useAdminLibraryPanel.ts b/web/src/pages/useAdminLibraryPanel.ts index b46c695..4ae33e1 100644 --- a/web/src/pages/useAdminLibraryPanel.ts +++ b/web/src/pages/useAdminLibraryPanel.ts @@ -32,20 +32,32 @@ function useCreateLibraryForm(refresh: () => Promise) { const [roots, setRoots] = useState([emptyRootDraft()]) const [type, setType] = useState('movie') const [coverURL, setCoverURL] = useState('') + const [createPerSubfolder, setCreatePerSubfolder] = useState(false) const handleCreate = async (e: FormEvent) => { e.preventDefault() try { - const payload = createRootPayload(roots) - if (payload.length === 0) { - toast.error('请至少填写一个路径') - return + if (createPerSubfolder) { + const parentPath = roots[0]?.path?.trim() + if (!parentPath) { + toast.error('请先选择或填写父级目录') + return + } + const { libraries } = await libraryAPI.createPerSubfolder(parentPath, type, coverURL.trim()) + toast.success(`已按目录创建 ${libraries.length} 个媒体库`) + } else { + const payload = createRootPayload(roots) + if (payload.length === 0) { + toast.error('请至少填写一个路径') + return + } + await libraryAPI.createWithRoots(name, type, payload, coverURL.trim()) + toast.success('媒体库已保存') } - await libraryAPI.createWithRoots(name, type, payload, coverURL.trim()) - toast.success('媒体库已保存') setName('') setRoots([emptyRootDraft()]) setCoverURL('') + setCreatePerSubfolder(false) await refresh() } catch (err: unknown) { toast.error(apiErrorMessage(err, '创建失败')) @@ -61,9 +73,11 @@ function useCreateLibraryForm(refresh: () => Promise) { type, coverURL, roots, + createPerSubfolder, setName, setType, setCoverURL, + setCreatePerSubfolder, updateRoot, addRoot: () => setRoots((prev) => [...prev, emptyRootDraft()]), removeRoot: (index: number) => setRoots((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index))),