From a6f2d0208dd208aa42a8ac9c95f1d200bba27e1d Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:53:49 +0800 Subject: [PATCH] feat: add STRM output directory presets --- .../handler/routes_authenticated_features.go | 1 + internal/handler/strm.go | 11 +++ internal/service/strm_output_presets.go | 79 +++++++++++++++++ internal/service/strm_output_presets_test.go | 86 +++++++++++++++++++ web/src/api/strm.ts | 8 ++ web/src/pages/StrmGenerateSection.tsx | 3 +- web/src/pages/StrmGenerateSectionParts.tsx | 10 ++- web/src/pages/StrmOutputDirPicker.tsx | 50 +++++++++++ web/src/pages/StrmPage.tsx | 5 +- web/src/pages/StrmRepairSection.tsx | 14 ++- web/src/pages/StrmTreeGenerateSection.tsx | 14 ++- web/src/pages/useStrmPage.ts | 4 + 12 files changed, 271 insertions(+), 14 deletions(-) create mode 100644 internal/service/strm_output_presets.go create mode 100644 internal/service/strm_output_presets_test.go create mode 100644 web/src/pages/StrmOutputDirPicker.tsx diff --git a/internal/handler/routes_authenticated_features.go b/internal/handler/routes_authenticated_features.go index c4096f1..b902768 100644 --- a/internal/handler/routes_authenticated_features.go +++ b/internal/handler/routes_authenticated_features.go @@ -52,6 +52,7 @@ func registerAuthedDLNARoutes(authed *gin.RouterGroup, svc *service.Container) { func registerAuthedSTRMRoutes(authed *gin.RouterGroup, svc *service.Container) { authed.PUT("/media/:id/strm", middleware.AdminRequired(), setSTRMHandler(svc)) authed.DELETE("/media/:id/strm", middleware.AdminRequired(), clearSTRMHandler(svc)) + authed.GET("/strm/output-presets", middleware.AdminRequired(), listSTRMOutputPresetsHandler(svc)) authed.POST("/strm/import", middleware.AdminRequired(), importSTRMHandler(svc)) authed.POST("/strm/generate", middleware.AdminRequired(), generateSTRMHandler(svc)) authed.POST("/strm/generate-from-tree", middleware.AdminRequired(), generateSTRMFromTreeHandler(svc)) diff --git a/internal/handler/strm.go b/internal/handler/strm.go index 944ca39..eb49596 100644 --- a/internal/handler/strm.go +++ b/internal/handler/strm.go @@ -134,6 +134,17 @@ type repairSTRMReq struct { RefreshLibrary bool `json:"refresh_library"` } +func listSTRMOutputPresetsHandler(svc *service.Container) gin.HandlerFunc { + return func(c *gin.Context) { + items, err := service.STRMOutputPresets(c.Request.Context(), svc.Repo) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"items": items}) + } +} + func generateSTRMHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { var req generateSTRMReq diff --git a/internal/service/strm_output_presets.go b/internal/service/strm_output_presets.go new file mode 100644 index 0000000..79394e9 --- /dev/null +++ b/internal/service/strm_output_presets.go @@ -0,0 +1,79 @@ +package service + +import ( + "context" + "path/filepath" + "strings" + + "github.com/ShukeBta/MediaStationGo/internal/model" + "github.com/ShukeBta/MediaStationGo/internal/repository" +) + +type STRMOutputPreset struct { + Label string `json:"label"` + Path string `json:"path"` + Kind string `json:"kind"` +} + +func STRMOutputPresets(ctx context.Context, repo *repository.Container) ([]STRMOutputPreset, error) { + presets := defaultSTRMOutputPresets() + seen := make(map[string]struct{}, len(presets)) + for _, preset := range presets { + seen[strmOutputPresetKey(preset.Path)] = struct{}{} + } + if repo == nil || repo.Library == nil { + return presets, nil + } + libraries, err := repo.Library.List(ctx) + if err != nil { + return nil, err + } + for i := range libraries { + lib := libraries[i] + if !lib.Enabled { + continue + } + roots, err := repo.Library.ListRoots(ctx, lib.ID) + if err != nil { + return nil, err + } + if len(roots) == 0 { + roots = []model.LibraryRoot{{Path: lib.Path, Enabled: lib.Enabled}} + } + for j := range roots { + root := roots[j] + if !root.Enabled || strings.TrimSpace(root.Path) == "" { + continue + } + if _, ok := ParseCloudLibraryMount(root.Path); ok { + continue + } + pathValue := filepath.Clean(resolveMappedDestinationPath(root.Path)) + if pathValue == "" || pathValue == "." { + continue + } + key := strmOutputPresetKey(pathValue) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + presets = append(presets, STRMOutputPreset{ + Label: lib.Name, + Path: pathValue, + Kind: "library", + }) + } + } + return presets, nil +} + +func defaultSTRMOutputPresets() []STRMOutputPreset { + return []STRMOutputPreset{ + {Label: "STRM 根目录", Path: filepath.Clean("data/strm"), Kind: "default"}, + {Label: "目录树 STRM", Path: filepath.Clean("data/strm/tree"), Kind: "default"}, + } +} + +func strmOutputPresetKey(pathValue string) string { + return strings.ToLower(filepath.Clean(strings.TrimSpace(pathValue))) +} diff --git a/internal/service/strm_output_presets_test.go b/internal/service/strm_output_presets_test.go new file mode 100644 index 0000000..3648bd1 --- /dev/null +++ b/internal/service/strm_output_presets_test.go @@ -0,0 +1,86 @@ +package service + +import ( + "path/filepath" + "testing" + + "github.com/ShukeBta/MediaStationGo/internal/model" + "github.com/ShukeBta/MediaStationGo/internal/repository" +) + +func TestSTRMOutputPresetsIncludesDefaultsAndLocalLibraries(t *testing.T) { + db := newServiceTestDB(t, &model.Library{}, &model.LibraryRoot{}) + repos := repository.New(db) + base := t.TempDir() + movieRoot := filepath.Join(base, "strm", "电影") + tvRoot := filepath.Join(base, "strm", "电视剧") + cloudRoot := BuildCloudLibraryPath("openlist", "/电影", "/电影") + disabledRoot := filepath.Join(base, "strm", "动漫") + + libraries := []*model.Library{ + {Name: "电影 STRM", Path: movieRoot, Type: "movie", Enabled: true}, + {Name: "电视剧 STRM", Path: tvRoot, Type: "tv", Enabled: true}, + {Name: "云盘", Path: cloudRoot, Type: "movie", Enabled: true}, + {Name: "停用", Path: disabledRoot, Type: "tv", Enabled: false}, + } + for _, lib := range libraries { + if err := repos.Library.Create(t.Context(), lib); err != nil { + t.Fatal(err) + } + } + if err := repos.DB.Model(&model.Library{}).Where("id = ?", libraries[3].ID).Update("enabled", false).Error; err != nil { + t.Fatal(err) + } + + presets, err := STRMOutputPresets(t.Context(), repos) + if err != nil { + t.Fatal(err) + } + got := map[string]STRMOutputPreset{} + for _, preset := range presets { + got[preset.Path] = preset + } + + if got[filepath.Clean("data/strm")].Kind != "default" || got[filepath.Clean("data/strm/tree")].Kind != "default" { + t.Fatalf("defaults missing from presets: %#v", presets) + } + if got[movieRoot].Label != "电影 STRM" || got[movieRoot].Kind != "library" { + t.Fatalf("movie preset = %#v, want local library preset", got[movieRoot]) + } + if got[tvRoot].Label != "电视剧 STRM" || got[tvRoot].Kind != "library" { + t.Fatalf("tv preset = %#v, want local library preset", got[tvRoot]) + } + if _, ok := got[cloudRoot]; ok { + t.Fatalf("cloud library should not be an output preset: %#v", presets) + } + if _, ok := got[disabledRoot]; ok { + t.Fatalf("disabled library should not be an output preset: %#v", presets) + } +} + +func TestSTRMOutputPresetsDeduplicatesLibraryRoots(t *testing.T) { + db := newServiceTestDB(t, &model.Library{}, &model.LibraryRoot{}) + repos := repository.New(db) + root := filepath.Join(t.TempDir(), "strm") + lib := model.Library{Name: "STRM", Path: root, Type: "movie", Enabled: true} + if err := repos.Library.CreateWithRoots(t.Context(), &lib, []model.LibraryRoot{ + {Path: root, Enabled: true}, + {Path: root, Enabled: true}, + }); err != nil { + t.Fatal(err) + } + + presets, err := STRMOutputPresets(t.Context(), repos) + if err != nil { + t.Fatal(err) + } + count := 0 + for _, preset := range presets { + if preset.Path == root { + count++ + } + } + if count != 1 { + t.Fatalf("root preset count = %d, presets=%#v", count, presets) + } +} diff --git a/web/src/api/strm.ts b/web/src/api/strm.ts index ad8c664..e72455d 100644 --- a/web/src/api/strm.ts +++ b/web/src/api/strm.ts @@ -88,10 +88,18 @@ export type STRMRefreshResult = { }> } +export type STRMOutputPreset = { + label: string + path: string + kind: 'default' | 'library' | string +} + export const strmAPI = { set: (mediaID: string, url: string) => api.put(`/media/${mediaID}/strm`, { url }).then((r) => r.data), clear: (mediaID: string) => api.delete(`/media/${mediaID}/strm`).then((r) => r.data), + outputPresets: () => + api.get<{ items: STRMOutputPreset[] }>('/strm/output-presets').then((r) => r.data.items), importURL: (libraryID: string, title: string, url: string) => api.post('/strm/import', { library_id: libraryID, title, url }).then((r) => r.data), generate: (input: GenerateSTRMInput) => diff --git a/web/src/pages/StrmGenerateSection.tsx b/web/src/pages/StrmGenerateSection.tsx index 49abb83..f1d3378 100644 --- a/web/src/pages/StrmGenerateSection.tsx +++ b/web/src/pages/StrmGenerateSection.tsx @@ -1,6 +1,6 @@ import type { FormEvent } from 'react' -import type { GenerateSTRMResult } from '../api/strm' +import type { GenerateSTRMResult, STRMOutputPreset } from '../api/strm' import type { Library } from '../types' import type { CloudPlaybackMode } from './strmPageModel' import { @@ -17,6 +17,7 @@ export type StrmGenerateSectionProps = { generateLibraryID: string baseURL: string outputDir: string + outputPresets: STRMOutputPreset[] cloudPlaybackMode: CloudPlaybackMode strmPlaybackEnabled: boolean redirectProxyEnabled: boolean diff --git a/web/src/pages/StrmGenerateSectionParts.tsx b/web/src/pages/StrmGenerateSectionParts.tsx index 06b3ea6..c4a0f79 100644 --- a/web/src/pages/StrmGenerateSectionParts.tsx +++ b/web/src/pages/StrmGenerateSectionParts.tsx @@ -4,6 +4,7 @@ import type { GenerateSTRMResult } from '../api/strm' import type { Library } from '../types' import { currentOrigin, type CloudPlaybackMode } from './strmPageModel' import type { StrmGenerateSectionProps } from './StrmGenerateSection' +import { StrmOutputDirPicker } from './StrmOutputDirPicker' type PlaybackStatusProps = Pick< StrmGenerateSectionProps, @@ -141,6 +142,7 @@ type StrmGenerateFormProps = Pick< | 'generateLibraryID' | 'baseURL' | 'outputDir' + | 'outputPresets' | 'overwrite' | 'includeLocal' | 'preserveTree' @@ -161,6 +163,7 @@ export function StrmGenerateForm({ generateLibraryID, baseURL, outputDir, + outputPresets, overwrite, includeLocal, preserveTree, @@ -198,11 +201,12 @@ export function StrmGenerateForm({ - setOutputDir(e.target.value)} + onChange={setOutputDir} />