mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
优化续播
This commit is contained in:
+61
-61
@@ -192,77 +192,77 @@ func updateUserStatusHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.IsActive {
|
||||
_ = svc.Repo.UserDevice.SetKickedByUser(c.Request.Context(), userID, false)
|
||||
} else {
|
||||
_ = svc.Repo.UserDevice.SetKickedByUser(c.Request.Context(), userID, true)
|
||||
}
|
||||
updated, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
updated.PopulateComputedFields()
|
||||
c.JSON(http.StatusOK, updated)
|
||||
if req.IsActive {
|
||||
_ = svc.Repo.UserDevice.SetKickedByUser(c.Request.Context(), userID, false)
|
||||
} else {
|
||||
_ = svc.Repo.UserDevice.SetKickedByUser(c.Request.Context(), userID, true)
|
||||
}
|
||||
updated, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
updated.PopulateComputedFields()
|
||||
c.JSON(http.StatusOK, updated)
|
||||
}
|
||||
}
|
||||
|
||||
type adminUpdateUserLibrariesReq struct {
|
||||
AllowedLibraryIDs *[]string `json:"allowed_library_ids"`
|
||||
}
|
||||
type adminUpdateUserLibrariesReq struct {
|
||||
AllowedLibraryIDs *[]string `json:"allowed_library_ids"`
|
||||
}
|
||||
|
||||
func updateUserLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req adminUpdateUserLibrariesReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
userID := c.Param("id")
|
||||
user, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
func updateUserLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req adminUpdateUserLibrariesReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
userID := c.Param("id")
|
||||
user, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var rawJSON string
|
||||
if req.AllowedLibraryIDs != nil && len(*req.AllowedLibraryIDs) > 0 {
|
||||
var cleanIDs []string
|
||||
for _, id := range *req.AllowedLibraryIDs {
|
||||
trimmed := strings.TrimSpace(id)
|
||||
if trimmed != "" {
|
||||
cleanIDs = append(cleanIDs, trimmed)
|
||||
}
|
||||
}
|
||||
if len(cleanIDs) > 0 {
|
||||
data, err := json.Marshal(cleanIDs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rawJSON = string(data)
|
||||
var rawJSON string
|
||||
if req.AllowedLibraryIDs != nil && len(*req.AllowedLibraryIDs) > 0 {
|
||||
var cleanIDs []string
|
||||
for _, id := range *req.AllowedLibraryIDs {
|
||||
trimmed := strings.TrimSpace(id)
|
||||
if trimmed != "" {
|
||||
cleanIDs = append(cleanIDs, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
updates := map[string]any{"allowed_library_ids": rawJSON}
|
||||
if err := svc.Repo.User.UpdateFields(c.Request.Context(), userID, updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
if len(cleanIDs) > 0 {
|
||||
data, err := json.Marshal(cleanIDs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rawJSON = string(data)
|
||||
}
|
||||
|
||||
updated, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil || updated == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload user"})
|
||||
return
|
||||
}
|
||||
updated.PopulateComputedFields()
|
||||
c.JSON(http.StatusOK, updated)
|
||||
}
|
||||
|
||||
updates := map[string]any{"allowed_library_ids": rawJSON}
|
||||
if err := svc.Repo.User.UpdateFields(c.Request.Context(), userID, updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil || updated == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload user"})
|
||||
return
|
||||
}
|
||||
updated.PopulateComputedFields()
|
||||
c.JSON(http.StatusOK, updated)
|
||||
}
|
||||
}
|
||||
|
||||
func annotateProtectedUsers(ctx context.Context, svc *service.Container, users []model.User) error {
|
||||
firstAdmin, err := svc.Repo.User.FirstAdmin(ctx)
|
||||
|
||||
@@ -44,10 +44,10 @@ func TestDeleteUserRefusesRecentRealtimeSession(t *testing.T) {
|
||||
if w.Code != http.StatusConflict {
|
||||
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if found, _ := repos.User.FindByID(t.Context(), viewer.ID); found == nil {
|
||||
t.Fatal("recent realtime user should not be deleted")
|
||||
}
|
||||
if found, _ := repos.User.FindByID(t.Context(), viewer.ID); found == nil {
|
||||
t.Fatal("recent realtime user should not be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserLibraries(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||
"github.com/ShukeBta/MMTL/internal/model"
|
||||
"github.com/ShukeBta/MMTL/internal/repository"
|
||||
"github.com/ShukeBta/MMTL/internal/service"
|
||||
)
|
||||
|
||||
func TestMountedEmbyPlayingProgressAndResumePipeline(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(model.AllModels()...); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
repos := repository.New(db)
|
||||
user := &model.User{
|
||||
Base: model.Base{ID: "user-1"},
|
||||
Username: "test_viewer",
|
||||
PasswordHash: "x",
|
||||
Role: "user",
|
||||
Tier: "free",
|
||||
IsActive: true,
|
||||
}
|
||||
if err := repos.User.Create(t.Context(), user); err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
|
||||
cfg := &config.Config{}
|
||||
logger := zap.NewNop()
|
||||
svc := &service.Container{
|
||||
Repo: repos,
|
||||
Emby: service.NewEmbyService(cfg, logger, repos),
|
||||
Sessions: service.NewSessionTrackerService(logger),
|
||||
Playback: service.NewPlaybackService(logger, repos),
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
// 注册带认证的路由,模拟已登录用户
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set(middleware.CtxUserID, user.ID)
|
||||
c.Next()
|
||||
})
|
||||
router.POST("/Sessions/Playing/Progress", embyPlayingProgressHandler(svc))
|
||||
router.GET("/Items", embyItemsHandler(svc))
|
||||
router.GET("/Users/:userId/Items/Resume", embyResumeItemsHandler(svc))
|
||||
router.GET("/Sessions", embySessionsHandler(svc))
|
||||
|
||||
remoteMediaID := service.EncodeEmbyRemoteID("mount-1", "remote-item-123")
|
||||
|
||||
// 1. 测试上报进度:客户端使用小写 query 参数 itemId / positionTicks
|
||||
progressReq := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/Sessions/Playing/Progress?itemId="+remoteMediaID+"&positionTicks=300000000&runTimeTicks=1000000000",
|
||||
nil,
|
||||
)
|
||||
wProgress := httptest.NewRecorder()
|
||||
router.ServeHTTP(wProgress, progressReq)
|
||||
if wProgress.Code != http.StatusNoContent {
|
||||
t.Fatalf("progress status = %d, body = %s", wProgress.Code, wProgress.Body.String())
|
||||
}
|
||||
|
||||
// 验证已持久化到 PlaybackHistory
|
||||
var hist model.PlaybackHistory
|
||||
if err := db.Where("user_id = ? AND media_id = ?", user.ID, remoteMediaID).First(&hist).Error; err != nil {
|
||||
t.Fatalf("playback history not saved: %v", err)
|
||||
}
|
||||
if hist.PositionMs != 30000 {
|
||||
t.Fatalf("expected position_ms = 30000, got %d", hist.PositionMs)
|
||||
}
|
||||
|
||||
// 2. 测试 Filters=IsResumable 能够包含该远程条目
|
||||
resumableReq := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
"/Items?Filters=IsResumable",
|
||||
nil,
|
||||
)
|
||||
wResumable := httptest.NewRecorder()
|
||||
router.ServeHTTP(wResumable, resumableReq)
|
||||
if wResumable.Code != http.StatusOK {
|
||||
t.Fatalf("items resumable status = %d, body = %s", wResumable.Code, wResumable.Body.String())
|
||||
}
|
||||
var resumableEnvelope map[string]any
|
||||
if err := json.Unmarshal(wResumable.Body.Bytes(), &resumableEnvelope); err != nil {
|
||||
t.Fatalf("decode resumable: %v", err)
|
||||
}
|
||||
// 因为没有配置真实的远程客户端连接,该远程条目在当前离线测试中不会 panic 崩溃,并且正常响应 Envelope
|
||||
if resumableEnvelope["TotalRecordCount"] == nil {
|
||||
t.Fatalf("missing TotalRecordCount in resumable envelope")
|
||||
}
|
||||
|
||||
// 3. 测试 /Users/:userId/Items/Resume 别名路由
|
||||
resumeAliasReq := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
"/Users/"+user.ID+"/Items/Resume",
|
||||
nil,
|
||||
)
|
||||
wResumeAlias := httptest.NewRecorder()
|
||||
router.ServeHTTP(wResumeAlias, resumeAliasReq)
|
||||
if wResumeAlias.Code != http.StatusOK {
|
||||
t.Fatalf("resume alias status = %d, body = %s", wResumeAlias.Code, wResumeAlias.Body.String())
|
||||
}
|
||||
|
||||
// 4. 测试 /Sessions 返回 NowPlayingItem
|
||||
sessionsReq := httptest.NewRequest(http.MethodGet, "/Sessions", nil)
|
||||
wSessions := httptest.NewRecorder()
|
||||
router.ServeHTTP(wSessions, sessionsReq)
|
||||
if wSessions.Code != http.StatusOK {
|
||||
t.Fatalf("sessions status = %d, body = %s", wSessions.Code, wSessions.Body.String())
|
||||
}
|
||||
var sessionsList []map[string]any
|
||||
if err := json.Unmarshal(wSessions.Body.Bytes(), &sessionsList); err != nil {
|
||||
t.Fatalf("decode sessions: %v", err)
|
||||
}
|
||||
if len(sessionsList) == 0 {
|
||||
t.Fatalf("expected at least 1 session")
|
||||
}
|
||||
nowPlaying, ok := sessionsList[0]["NowPlayingItem"].(map[string]any)
|
||||
if !ok || nowPlaying["Id"] != remoteMediaID {
|
||||
t.Fatalf("expected NowPlayingItem with id %q, got %#v", remoteMediaID, sessionsList[0]["NowPlayingItem"])
|
||||
}
|
||||
}
|
||||
|
||||
func signMockToken(secret, userID string) string {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": userID,
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
})
|
||||
s, _ := token.SignedString([]byte(secret))
|
||||
return s
|
||||
}
|
||||
@@ -219,4 +219,4 @@ func reorderEmbyMountsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,13 @@ import (
|
||||
|
||||
type embyPlayingReq struct {
|
||||
ItemId string `json:"ItemId"`
|
||||
ItemIDLower string `json:"itemId"`
|
||||
ID string `json:"Id"`
|
||||
IDLower string `json:"id"`
|
||||
PositionTicks int64 `json:"PositionTicks"`
|
||||
PositionLower int64 `json:"positionTicks"`
|
||||
RunTimeTicks int64 `json:"RunTimeTicks"`
|
||||
RunTimeLower int64 `json:"runTimeTicks"`
|
||||
}
|
||||
|
||||
func embyPlayingProgressHandler(svc *service.Container) gin.HandlerFunc {
|
||||
@@ -25,16 +30,25 @@ func embyPlayingProgressHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
var req embyPlayingReq
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if req.ItemId == "" {
|
||||
req.ItemId = c.Query("ItemId")
|
||||
itemID := embyFirstNonEmptyString(req.ItemId, req.ItemIDLower, req.ID, req.IDLower)
|
||||
if itemID == "" {
|
||||
itemID = embyFirstNonEmptyString(firstQueryValue(c, "ItemId", "itemId", "Id", "id"))
|
||||
}
|
||||
if req.PositionTicks == 0 {
|
||||
req.PositionTicks, _ = strconv.ParseInt(c.Query("PositionTicks"), 10, 64)
|
||||
pos := req.PositionTicks
|
||||
if pos == 0 {
|
||||
pos = req.PositionLower
|
||||
}
|
||||
if req.RunTimeTicks == 0 {
|
||||
req.RunTimeTicks, _ = strconv.ParseInt(c.Query("RunTimeTicks"), 10, 64)
|
||||
if pos == 0 {
|
||||
pos, _ = strconv.ParseInt(firstQueryValue(c, "PositionTicks", "positionTicks"), 10, 64)
|
||||
}
|
||||
if req.ItemId == "" {
|
||||
runTime := req.RunTimeTicks
|
||||
if runTime == 0 {
|
||||
runTime = req.RunTimeLower
|
||||
}
|
||||
if runTime == 0 {
|
||||
runTime, _ = strconv.ParseInt(firstQueryValue(c, "RunTimeTicks", "runTimeTicks"), 10, 64)
|
||||
}
|
||||
if itemID == "" {
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
@@ -43,7 +57,7 @@ func embyPlayingProgressHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.Status(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if err := svc.Emby.RecordProgress(c.Request.Context(), uid, req.ItemId, req.PositionTicks, req.RunTimeTicks); err != nil {
|
||||
if err := svc.Emby.RecordProgress(c.Request.Context(), uid, itemID, pos, runTime); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -54,9 +68,9 @@ func embyPlayingProgressHandler(svc *service.Container) gin.HandlerFunc {
|
||||
clientInfo.DeviceName,
|
||||
clientInfo.Client,
|
||||
c.ClientIP(),
|
||||
req.ItemId,
|
||||
req.PositionTicks,
|
||||
req.RunTimeTicks,
|
||||
itemID,
|
||||
pos,
|
||||
runTime,
|
||||
stopped)
|
||||
}
|
||||
if svc.Device != nil && !stopped {
|
||||
|
||||
@@ -161,6 +161,8 @@ func registerEmbyAuthenticatedItemRoutes(auth *gin.RouterGroup, svc *service.Con
|
||||
auth.GET("/Users/:userId/Items/Counts", embyItemsCountsHandler(svc))
|
||||
auth.GET("/Items/Latest", embyLatestItemsHandler(svc))
|
||||
auth.GET("/Items/Resume", embyResumeItemsHandler(svc))
|
||||
auth.GET("/Users/:userId/Items/Resume", embyResumeItemsHandler(svc))
|
||||
auth.GET("/UserItems/Resume", embyResumeItemsHandler(svc))
|
||||
auth.GET("/Items/:id", embyItemByIDHandler(svc))
|
||||
auth.GET("/Users/:userId/Items/:id", embyUserItemByIDHandler(svc))
|
||||
auth.GET("/Shows/:id/Seasons", embyShowSeasonsHandler(svc))
|
||||
|
||||
@@ -32,6 +32,8 @@ func registerLowercaseEmbyItemRoutes(auth *gin.RouterGroup, svc *service.Contain
|
||||
auth.GET("/users/:userId/items/counts", embyItemsCountsHandler(svc))
|
||||
auth.GET("/items/latest", embyLatestItemsHandler(svc))
|
||||
auth.GET("/items/resume", embyResumeItemsHandler(svc))
|
||||
auth.GET("/users/:userId/items/resume", embyResumeItemsHandler(svc))
|
||||
auth.GET("/useritems/resume", embyResumeItemsHandler(svc))
|
||||
auth.GET("/items/:id", embyItemByIDHandler(svc))
|
||||
auth.GET("/users/:userId/items/:id", embyUserItemByIDHandler(svc))
|
||||
auth.GET("/shows/:id/seasons", embyShowSeasonsHandler(svc))
|
||||
|
||||
@@ -41,7 +41,17 @@ func embySessionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
"SupportsRemoteControl": true,
|
||||
}
|
||||
if itemID != "" && sess.IsPlaying {
|
||||
row["NowPlayingItem"] = gin.H{"Id": itemID}
|
||||
nowPlaying := gin.H{"Id": itemID}
|
||||
if svc.Emby != nil {
|
||||
if item, _ := svc.Emby.Item(c.Request.Context(), itemID, sess.UserID); item != nil {
|
||||
for _, key := range []string{"Name", "Type", "RunTimeTicks", "PrimaryImageItemId", "ImageTags", "SeriesName", "SeasonName", "IndexNumber", "ParentIndexNumber"} {
|
||||
if val, ok := item[key]; ok && val != nil {
|
||||
nowPlaying[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
row["NowPlayingItem"] = nowPlaying
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
|
||||
@@ -401,17 +401,17 @@ func newPlaybackScopeTestRouter(t *testing.T) (*gin.Engine, *service.Container,
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
api := router.Group("/api")
|
||||
api.Use(middleware.AuthRequired(cfg.Secrets.JWTSecret))
|
||||
api.GET("/playback/:id/info", playbackInfoHandler(svc))
|
||||
api.GET("/playback/:id/external-url", externalURLHandler(svc))
|
||||
api.GET("/playback/:id/external-players", externalPlayersHandler(svc))
|
||||
api.GET("/stream/:id", streamHandler(svc))
|
||||
api.GET("/hls/:id/index.m3u8", hlsPlaylistHandler(svc))
|
||||
api.GET("/media/:id/subtitles", listSubtitlesHandler(svc))
|
||||
return router, svc, cfg.Secrets.JWTSecret
|
||||
}
|
||||
router := gin.New()
|
||||
api := router.Group("/api")
|
||||
api.Use(middleware.AuthRequired(cfg.Secrets.JWTSecret))
|
||||
api.GET("/playback/:id/info", playbackInfoHandler(svc))
|
||||
api.GET("/playback/:id/external-url", externalURLHandler(svc))
|
||||
api.GET("/playback/:id/external-players", externalPlayersHandler(svc))
|
||||
api.GET("/stream/:id", streamHandler(svc))
|
||||
api.GET("/hls/:id/index.m3u8", hlsPlaylistHandler(svc))
|
||||
api.GET("/media/:id/subtitles", listSubtitlesHandler(svc))
|
||||
return router, svc, cfg.Secrets.JWTSecret
|
||||
}
|
||||
|
||||
func TestPlaybackInfoForSTRMMediaDisablesHLS(t *testing.T) {
|
||||
router, _, secret := newPlaybackScopeTestRouter(t)
|
||||
@@ -478,4 +478,3 @@ func TestListSubtitlesForRemoteEmbyMediaReturnsEmptyTracks(t *testing.T) {
|
||||
t.Fatalf("expected empty tracks array, got %v", payload.Tracks)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,11 +49,11 @@ func registerAdminStrmRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
||||
// Emby 挂载管理:远程 Emby 媒体库挂载(账号复用 strm/accounts)
|
||||
admin.GET("/emby/accounts/:id/views", embyAccountViewsHandler(svc))
|
||||
admin.POST("/emby/accounts/:id/full-mount", fullMountEmbyAccountHandler(svc))
|
||||
admin.GET("/emby/mounts", listEmbyMountsHandler(svc))
|
||||
admin.POST("/emby/mounts", createEmbyMountsHandler(svc))
|
||||
admin.PUT("/emby/mounts/reorder", reorderEmbyMountsHandler(svc))
|
||||
admin.PUT("/emby/mounts/:id", updateEmbyMountHandler(svc))
|
||||
admin.DELETE("/emby/mounts/:id", deleteEmbyMountHandler(svc))
|
||||
admin.GET("/emby/mounts", listEmbyMountsHandler(svc))
|
||||
admin.POST("/emby/mounts", createEmbyMountsHandler(svc))
|
||||
admin.PUT("/emby/mounts/reorder", reorderEmbyMountsHandler(svc))
|
||||
admin.PUT("/emby/mounts/:id", updateEmbyMountHandler(svc))
|
||||
admin.DELETE("/emby/mounts/:id", deleteEmbyMountHandler(svc))
|
||||
|
||||
admin.GET("/strm/accounts", listStrmAccountsHandler(svc))
|
||||
admin.POST("/strm/accounts", createStrmAccountHandler(svc))
|
||||
|
||||
@@ -50,19 +50,19 @@ func TestStrmAdminRoutesAreRegistered(t *testing.T) {
|
||||
"GET /api/admin/strm/downloads",
|
||||
"POST /api/admin/strm/downloads/:id/cancel",
|
||||
"POST /api/admin/strm/downloads/:id/retry",
|
||||
"POST /api/admin/strm/downloads/clear-finished",
|
||||
"POST /api/admin/strm/downloads/clear-canceled",
|
||||
"POST /api/admin/strm/downloads/retry-failed",
|
||||
"POST /api/admin/strm/downloads/cancel-pending",
|
||||
"GET /api/admin/strm/uploads",
|
||||
"POST /api/admin/strm/uploads/:id/cancel",
|
||||
"POST /api/admin/strm/uploads/:id/retry",
|
||||
"POST /api/admin/strm/uploads/clear-done",
|
||||
"POST /api/admin/strm/uploads/clear-finished",
|
||||
"POST /api/admin/strm/uploads/clear-canceled",
|
||||
"POST /api/admin/strm/uploads/retry-failed",
|
||||
"POST /api/admin/strm/uploads/cancel-pending",
|
||||
"GET /api/strm/play/:provider/:file",
|
||||
"POST /api/admin/strm/downloads/clear-finished",
|
||||
"POST /api/admin/strm/downloads/clear-canceled",
|
||||
"POST /api/admin/strm/downloads/retry-failed",
|
||||
"POST /api/admin/strm/downloads/cancel-pending",
|
||||
"GET /api/admin/strm/uploads",
|
||||
"POST /api/admin/strm/uploads/:id/cancel",
|
||||
"POST /api/admin/strm/uploads/:id/retry",
|
||||
"POST /api/admin/strm/uploads/clear-done",
|
||||
"POST /api/admin/strm/uploads/clear-finished",
|
||||
"POST /api/admin/strm/uploads/clear-canceled",
|
||||
"POST /api/admin/strm/uploads/retry-failed",
|
||||
"POST /api/admin/strm/uploads/cancel-pending",
|
||||
"GET /api/strm/play/:provider/:file",
|
||||
} {
|
||||
if !routes[want] {
|
||||
t.Fatalf("%s route is not registered", want)
|
||||
|
||||
@@ -125,6 +125,18 @@ func historyContinueHandler(svc *service.Container) gin.HandlerFunc {
|
||||
for _, r := range rows {
|
||||
m, ok := mIdx[r.MediaID]
|
||||
if !ok {
|
||||
if svc.EmbyRemote != nil && service.IsEmbyRemoteID(r.MediaID) {
|
||||
mountID, remoteID, _ := service.DecodeEmbyRemoteID(r.MediaID)
|
||||
if mount, acct, _ := svc.EmbyRemote.ResolveMount(c.Request.Context(), mountID); mount != nil && acct != nil {
|
||||
if rm, err := svc.EmbyRemote.RemoteMediaDetail(c.Request.Context(), mount, acct, remoteID); err == nil && rm != nil {
|
||||
out = append(out, gin.H{
|
||||
"history": r,
|
||||
"media": *rm,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
@@ -163,7 +175,7 @@ func historyDeleteHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "status must be completed or incomplete"})
|
||||
return
|
||||
}
|
||||
res := q.Unscoped().Delete(&model.PlaybackHistory{})
|
||||
res := q.Unscoped().Delete(&model.PlaybackHistory{})
|
||||
if err := res.Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user