mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 19:46:38 +08:00
Add library covers and configurable footer visibility
This commit is contained in:
@@ -18,6 +18,7 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
|
||||
{
|
||||
api.GET("/health", healthCheck)
|
||||
api.GET("/version", versionInfo)
|
||||
api.GET("/public/ui-config", publicUIConfigHandler(svc))
|
||||
|
||||
// Telegram Bot webhook — called by Telegram servers, no auth.
|
||||
api.POST("/telegram/webhook", telegramWebhookHandler(svc))
|
||||
|
||||
@@ -16,11 +16,12 @@ 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
@@ -98,7 +99,7 @@ func createLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
if len(roots) == 0 && strings.TrimSpace(req.Path) != "" {
|
||||
roots = append(roots, service.LibraryRootInput{Path: req.Path})
|
||||
}
|
||||
l, err := svc.Media.CreateLibraryWithRoots(c.Request.Context(), req.Name, req.Type, roots)
|
||||
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
|
||||
@@ -111,6 +112,30 @@ func createLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
type updateLibraryReq struct {
|
||||
CoverURL string `json:"cover_url"`
|
||||
}
|
||||
|
||||
func updateLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req updateLibraryReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := svc.Media.UpdateLibraryCover(c.Request.Context(), c.Param("id"), req.CoverURL); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
lib, err := svc.Repo.Library.FindByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil || lib == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "library not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, lib)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
const hideCommunityLinksSettingKey = "ui.hide_community_links_for_users"
|
||||
|
||||
func publicUIConfigHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
hide := false
|
||||
if svc != nil && svc.Repo != nil && svc.Repo.Setting != nil {
|
||||
if value, err := svc.Repo.Setting.Get(c.Request.Context(), hideCommunityLinksSettingKey); err == nil {
|
||||
hide = value == "true" || value == "1"
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"hide_community_links_for_users": hide})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
func TestPublicUIConfigExposesCommunityLinkVisibility(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.Setting{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
if err := repos.Setting.Set(t.Context(), hideCommunityLinksSettingKey, "true"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
svc := &service.Container{Repo: repos, Log: zap.NewNop()}
|
||||
r := gin.New()
|
||||
r.GET("/api/public/ui-config", publicUIConfigHandler(svc))
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/public/ui-config", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK || !strings.Contains(resp.Body.String(), `"hide_community_links_for_users":true`) {
|
||||
t.Fatalf("status=%d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ func registerAuthedLibraryRoutes(authed *gin.RouterGroup, svc *service.Container
|
||||
authed.GET("/libraries", listLibrariesHandler(svc))
|
||||
authed.POST("/libraries", middleware.AdminRequired(), createLibraryHandler(svc))
|
||||
authed.GET("/libraries/:id", getLibraryHandler(svc))
|
||||
authed.PATCH("/libraries/:id", middleware.AdminRequired(), updateLibraryHandler(svc))
|
||||
authed.DELETE("/libraries/:id", middleware.AdminRequired(), deleteLibraryHandler(svc))
|
||||
authed.GET("/libraries/:id/roots", middleware.AdminRequired(), listLibraryRootsHandler(svc))
|
||||
authed.POST("/libraries/:id/roots", middleware.AdminRequired(), createLibraryRootHandler(svc))
|
||||
|
||||
@@ -3,11 +3,12 @@ package model
|
||||
// Library 表示一个逻辑媒体库。Path 保留为兼容字段,指向第一条 LibraryRoot。
|
||||
type Library struct {
|
||||
Base
|
||||
Name string `gorm:"size:128;not null" json:"name"`
|
||||
Path string `gorm:"size:1024;not null" json:"path"`
|
||||
Type string `gorm:"size:16;not null;default:movie" json:"type"` // movie / tv / anime / music
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
Roots []LibraryRoot `gorm:"foreignKey:LibraryID" json:"roots,omitempty"`
|
||||
Name string `gorm:"size:128;not null" json:"name"`
|
||||
Path string `gorm:"size:1024;not null" json:"path"`
|
||||
Type string `gorm:"size:16;not null;default:movie" json:"type"` // movie / tv / anime / music
|
||||
CoverURL string `gorm:"size:1024" json:"cover_url,omitempty"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
Roots []LibraryRoot `gorm:"foreignKey:LibraryID" json:"roots,omitempty"`
|
||||
}
|
||||
|
||||
// LibraryRoot 是逻辑媒体库下的一条真实物理/挂载路径。
|
||||
|
||||
@@ -24,6 +24,10 @@ func (s *MediaService) CreateLibrary(ctx context.Context, name, path, kind strin
|
||||
}
|
||||
|
||||
func (s *MediaService) CreateLibraryWithRoots(ctx context.Context, name, kind string, inputs []LibraryRootInput) (*model.Library, error) {
|
||||
return s.CreateLibraryWithRootsAndCover(ctx, name, kind, "", inputs)
|
||||
}
|
||||
|
||||
func (s *MediaService) CreateLibraryWithRootsAndCover(ctx context.Context, name, kind, coverURL string, inputs []LibraryRootInput) (*model.Library, error) {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, errors.New("name required")
|
||||
}
|
||||
@@ -35,6 +39,11 @@ func (s *MediaService) CreateLibraryWithRoots(ctx context.Context, name, kind st
|
||||
if existing, err := s.findLogicalLibrary(ctx, name, kind); err != nil {
|
||||
return nil, err
|
||||
} else if existing != nil {
|
||||
if strings.TrimSpace(coverURL) != "" {
|
||||
if err := s.UpdateLibraryCover(ctx, existing.ID, coverURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
lib, err := s.appendLibraryRoots(ctx, existing, roots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -42,7 +51,7 @@ func (s *MediaService) CreateLibraryWithRoots(ctx context.Context, name, kind st
|
||||
s.invalidateMediaCache(ctx)
|
||||
return lib, nil
|
||||
}
|
||||
lib := &model.Library{Name: strings.TrimSpace(name), Path: roots[0].Path, Type: kind, Enabled: true}
|
||||
lib := &model.Library{Name: strings.TrimSpace(name), Path: roots[0].Path, Type: kind, CoverURL: strings.TrimSpace(coverURL), Enabled: true}
|
||||
if err := s.repo.Library.CreateWithRoots(ctx, lib, roots); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,6 +59,11 @@ func (s *MediaService) CreateLibraryWithRoots(ctx context.Context, name, kind st
|
||||
return lib, 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
|
||||
}
|
||||
|
||||
func (s *MediaService) findLogicalLibrary(ctx context.Context, name, kind string) (*model.Library, error) {
|
||||
if s == nil || s.repo == nil || s.repo.Library == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -53,6 +53,30 @@ func TestCreateLibraryWithRootsAppendsToExistingLogicalLibrary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateLibraryWithRootsStoresAndUpdatesCustomCover(t *testing.T) {
|
||||
db := newServiceTestDB(t, &model.Library{}, &model.LibraryRoot{}, &model.Media{})
|
||||
repos := repository.New(db)
|
||||
svc := NewMediaService(&config.Config{}, zap.NewNop(), repos)
|
||||
|
||||
lib, err := svc.CreateLibraryWithRootsAndCover(t.Context(), "收藏", "movie", "https://example.com/cover.jpg", []LibraryRootInput{{Path: t.TempDir()}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if lib.CoverURL != "https://example.com/cover.jpg" {
|
||||
t.Fatalf("cover_url = %q", lib.CoverURL)
|
||||
}
|
||||
if err := svc.UpdateLibraryCover(t.Context(), lib.ID, "https://example.com/new.jpg"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updated, err := repos.Library.FindByID(t.Context(), lib.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if updated.CoverURL != "https://example.com/new.jpg" {
|
||||
t.Fatalf("updated cover_url = %q", updated.CoverURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateLibraryWithRootsKeepsDifferentTypesSeparate(t *testing.T) {
|
||||
rootMovie := t.TempDir()
|
||||
rootTV := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user