mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
fix: stabilize library paths and subscription matching
This commit is contained in:
@@ -31,7 +31,6 @@ func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
libs = service.FilterDeprecatedNativeCloudLibraries(libs)
|
||||
libs = service.FilterInternalCloudAutoCategoryLibraries(libs)
|
||||
role, _ := c.Get(middleware.CtxUserRole)
|
||||
includeHidden := role == "admin" && (c.Query("include_hidden") == "1" || c.Query("all") == "1")
|
||||
if !includeHidden {
|
||||
@@ -45,6 +44,7 @@ func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
libs = filtered
|
||||
} else {
|
||||
libs = service.FilterMergedCloudAutoCategoryLibraries(libs)
|
||||
libs = service.NormalizeCloudLibraryDisplayNames(libs)
|
||||
}
|
||||
c.JSON(http.StatusOK, libs)
|
||||
@@ -63,7 +63,6 @@ func getLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
libs := service.FilterDeprecatedNativeCloudLibraries([]model.Library{*lib})
|
||||
libs = service.FilterInternalCloudAutoCategoryLibraries(libs)
|
||||
if len(libs) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestListLibrariesIncludeHiddenNormalizesCloudDisplayNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLibrariesIncludeHiddenHidesInternalAutoCategoryLibraries(t *testing.T) {
|
||||
func TestListLibrariesShowsAutoCategoryLibraries(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
@@ -118,8 +118,32 @@ func TestListLibrariesIncludeHiddenHidesInternalAutoCategoryLibraries(t *testing
|
||||
}
|
||||
|
||||
all := requestLibraries(t, svc, "admin", "admin", "/api/libraries?include_hidden=1")
|
||||
if len(all) != 1 || all[0].ID != root.ID {
|
||||
t.Fatalf("include_hidden list = %#v, want only user-mounted cloud library", all)
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("include_hidden list = %#v, want root plus auto category library", all)
|
||||
}
|
||||
ids := map[string]bool{}
|
||||
for _, lib := range all {
|
||||
ids[lib.ID] = true
|
||||
}
|
||||
if !ids[root.ID] || !ids[auto.ID] {
|
||||
t.Fatalf("include_hidden list = %#v, want root %s and auto category %s", all, root.ID, auto.ID)
|
||||
}
|
||||
|
||||
visible := requestLibraries(t, svc, "user-1", "user", "/api/libraries")
|
||||
if len(visible) != 2 {
|
||||
t.Fatalf("visible list = %#v, want root plus auto category library", visible)
|
||||
}
|
||||
ids = map[string]bool{}
|
||||
for _, lib := range visible {
|
||||
ids[lib.ID] = true
|
||||
}
|
||||
if !ids[root.ID] || !ids[auto.ID] {
|
||||
t.Fatalf("visible list = %#v, want root %s and auto category %s", visible, root.ID, auto.ID)
|
||||
}
|
||||
|
||||
got := requestLibrary(t, svc, "user-1", "user", "/api/libraries/"+auto.ID, auto.ID)
|
||||
if got.ID != auto.ID || got.Name != auto.Name {
|
||||
t.Fatalf("auto category detail = %#v, want accessible category library", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
func recognitionWordsService(svc *service.Container) *service.RecognitionWordsService {
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
if svc.RecognitionWords != nil {
|
||||
return svc.RecognitionWords
|
||||
}
|
||||
return service.NewRecognitionWordsService(svc.Log, svc.Repo)
|
||||
}
|
||||
|
||||
func getRecognitionWordsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
rw := recognitionWordsService(svc)
|
||||
if rw == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "recognition words service unavailable"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, rw.Config(c.Request.Context()))
|
||||
}
|
||||
}
|
||||
|
||||
func saveRecognitionWordsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
rw := recognitionWordsService(svc)
|
||||
if rw == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "recognition words service unavailable"})
|
||||
return
|
||||
}
|
||||
var req service.RecognitionWordsConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := rw.SaveConfig(c.Request.Context(), req); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, rw.Config(c.Request.Context()))
|
||||
}
|
||||
}
|
||||
|
||||
func syncRecognitionWordsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
rw := recognitionWordsService(svc)
|
||||
if rw == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "recognition words service unavailable"})
|
||||
return
|
||||
}
|
||||
cfg, err := rw.SyncShared(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func testRecognitionWordsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
rw := recognitionWordsService(svc)
|
||||
if rw == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "recognition words service unavailable"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Input string `json:"input"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, rw.Test(c.Request.Context(), req.Input))
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ func registerAdminRoutes(api *gin.RouterGroup, cfg *config.Config, svc *service.
|
||||
registerAdminRepairRoutes(admin, svc)
|
||||
registerAdminAPIConfigRoutes(admin, svc)
|
||||
registerAdminSchedulerRoutes(admin, svc)
|
||||
registerAdminRecognitionWordRoutes(admin, svc)
|
||||
}
|
||||
|
||||
func registerAdminUserRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
||||
@@ -130,3 +131,10 @@ func registerAdminSchedulerRoutes(admin *gin.RouterGroup, svc *service.Container
|
||||
admin.GET("/scheduler", schedulerStatusHandler(svc))
|
||||
admin.POST("/scheduler/:name/run", schedulerRunHandler(svc))
|
||||
}
|
||||
|
||||
func registerAdminRecognitionWordRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
||||
admin.GET("/recognition-words", getRecognitionWordsHandler(svc))
|
||||
admin.PUT("/recognition-words", saveRecognitionWordsHandler(svc))
|
||||
admin.POST("/recognition-words/sync", syncRecognitionWordsHandler(svc))
|
||||
admin.POST("/recognition-words/test", testRecognitionWordsHandler(svc))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -23,19 +24,13 @@ func siteResourceHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "keyword required"})
|
||||
return
|
||||
}
|
||||
all, err := svc.Site.Search(c.Request.Context(), keyword)
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
items, err := svc.Site.SearchSite(c.Request.Context(), c.Param("id"), keyword, page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
want := c.Param("id")
|
||||
filtered := make([]service.SearchResult, 0, len(all))
|
||||
for _, r := range all {
|
||||
if r.SiteID == want {
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": filtered})
|
||||
c.JSON(http.StatusOK, gin.H{"items": items, "total": len(items)})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
@@ -56,9 +57,17 @@ func updateSubscriptionHandler(svc *service.Container) gin.HandlerFunc {
|
||||
Model(&model.Subscription{}).
|
||||
Where("id = ?", c.Param("id")).
|
||||
Updates(updates).Error; err != nil {
|
||||
logSubscriptionWarn(svc, "subscription update failed",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.String("subscription_id", c.Param("id")),
|
||||
zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
logSubscriptionInfo(svc, "subscription updated",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.String("subscription_id", c.Param("id")),
|
||||
zap.Strings("fields", subscriptionUpdateFieldNames(updates)))
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@@ -146,6 +155,14 @@ func subscriptionPatchUpdates(patch subscriptionPatchReq) map[string]any {
|
||||
return updates
|
||||
}
|
||||
|
||||
func subscriptionUpdateFieldNames(updates map[string]any) []string {
|
||||
names := make([]string, 0, len(updates))
|
||||
for name := range updates {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// searchSubscriptionHandler runs a one-off keyword search against the
|
||||
// configured tracker sites for the given subscription. We treat the
|
||||
// subscription's filter as the search term; this lets the UI preview
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
func subscriptionRequestUserID(c *gin.Context) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
if uid, ok := c.Get(middleware.CtxUserID); ok {
|
||||
if userID, ok := uid.(string); ok {
|
||||
return userID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func subscriptionFeedKind(feedURL string) string {
|
||||
raw := strings.TrimSpace(feedURL)
|
||||
if raw == "" {
|
||||
return "empty"
|
||||
}
|
||||
lower := strings.ToLower(raw)
|
||||
if strings.HasPrefix(lower, "site-search://") {
|
||||
return "site-search"
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err == nil && parsed.Scheme != "" {
|
||||
return parsed.Scheme
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func logSubscriptionInfo(svc *service.Container, msg string, fields ...zap.Field) {
|
||||
if svc == nil || svc.Log == nil {
|
||||
return
|
||||
}
|
||||
svc.Log.Info(msg, fields...)
|
||||
}
|
||||
|
||||
func logSubscriptionWarn(svc *service.Container, msg string, fields ...zap.Field) {
|
||||
if svc == nil || svc.Log == nil {
|
||||
return
|
||||
}
|
||||
svc.Log.Warn(msg, fields...)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
@@ -80,9 +81,24 @@ func createSubscriptionHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
enrichSubscriptionArtwork(c.Request.Context(), svc, s)
|
||||
if err := svc.Subscription.Create(c.Request.Context(), s); err != nil {
|
||||
logSubscriptionWarn(svc, "subscription create failed",
|
||||
zap.String("user_id", s.UserID),
|
||||
zap.String("name", req.Name),
|
||||
zap.String("feed_kind", subscriptionFeedKind(req.FeedURL)),
|
||||
zap.Bool("enabled", enabled),
|
||||
zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
logSubscriptionInfo(svc, "subscription created",
|
||||
zap.String("user_id", s.UserID),
|
||||
zap.String("subscription_id", s.ID),
|
||||
zap.String("name", s.Name),
|
||||
zap.String("feed_kind", subscriptionFeedKind(s.FeedURL)),
|
||||
zap.String("media_type", s.MediaType),
|
||||
zap.String("media_category", s.MediaCategory),
|
||||
zap.Bool("enabled", s.Enabled),
|
||||
zap.Bool("wash_enabled", s.WashEnabled))
|
||||
enriched := []model.Subscription{*s}
|
||||
svc.Subscription.EnrichManagementProgress(c.Request.Context(), enriched)
|
||||
*s = enriched[0]
|
||||
@@ -94,11 +110,18 @@ func listSubscriptionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
items, err := svc.Subscription.List(c.Request.Context())
|
||||
if err != nil {
|
||||
logSubscriptionWarn(svc, "subscription list failed",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
svc.Subscription.EnrichManagementProgress(c.Request.Context(), items)
|
||||
go enrichAndPersistSubscriptions(context.Background(), svc, append([]model.Subscription(nil), items...))
|
||||
logSubscriptionInfo(svc, "subscription list returned",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.Int("count", len(items)),
|
||||
zap.Bool("history", false))
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
}
|
||||
@@ -107,10 +130,17 @@ func listSubscriptionHistoryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
items, err := svc.Subscription.History(c.Request.Context())
|
||||
if err != nil {
|
||||
logSubscriptionWarn(svc, "subscription history list failed",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
svc.Subscription.EnrichManagementProgress(c.Request.Context(), items)
|
||||
logSubscriptionInfo(svc, "subscription list returned",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.Int("count", len(items)),
|
||||
zap.Bool("history", true))
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
}
|
||||
@@ -118,9 +148,16 @@ func listSubscriptionHistoryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
func deleteSubscriptionHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if err := svc.Subscription.Delete(c.Request.Context(), c.Param("id")); err != nil {
|
||||
logSubscriptionWarn(svc, "subscription delete failed",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.String("subscription_id", c.Param("id")),
|
||||
zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
logSubscriptionInfo(svc, "subscription deleted",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.String("subscription_id", c.Param("id")))
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@@ -129,9 +166,17 @@ func runSubscriptionHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
n, err := svc.Subscription.RunNow(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
logSubscriptionWarn(svc, "subscription run now failed",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.String("subscription_id", c.Param("id")),
|
||||
zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
logSubscriptionInfo(svc, "subscription run now completed",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.String("subscription_id", c.Param("id")),
|
||||
zap.Int("queued", n))
|
||||
c.JSON(http.StatusOK, gin.H{"queued": n})
|
||||
}
|
||||
}
|
||||
@@ -140,9 +185,17 @@ func restoreSubscriptionHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
sub, err := svc.Subscription.Restore(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
logSubscriptionWarn(svc, "subscription restore failed",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.String("subscription_id", c.Param("id")),
|
||||
zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
logSubscriptionInfo(svc, "subscription restored",
|
||||
zap.String("user_id", subscriptionRequestUserID(c)),
|
||||
zap.String("subscription_id", sub.ID),
|
||||
zap.String("name", sub.Name))
|
||||
enriched := []model.Subscription{*sub}
|
||||
svc.Subscription.EnrichManagementProgress(c.Request.Context(), enriched)
|
||||
c.JSON(http.StatusOK, enriched[0])
|
||||
|
||||
Reference in New Issue
Block a user