mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
73b95a8e38
- 新增 EmbyMount 表与 /admin/emby/mounts CRUD:把远程 Emby 的媒体库按需
挂载到本项目(同一服务器可选择性挂载/全量挂载),代理开关下沉到每个
挂载(一个 Emby 服务器可部分库代理、部分库直连)
- 伪装 ID 改为 embyremote~{mountID}~{remoteID}:播放/详情/状态/图片链路
全部经 ResolveMount 解析挂载与账号
- 新增「Emby 挂载」独立菜单页(/emby-mount):账号管理(添加/测试/编辑/
删除)+ 挂载列表(代理切换/停用/取消)+ 选择挂载对话框(多选+全量)
- STRM 管理页过滤 emby_remote 账号;账号表单移除 emby 类型
- Boot 幂等 AutoSeedMounts:旧 emby 账号自动全量挂载(沿用旧代理配置),
升级后媒体库不消失
- 修复:远程流请求注入浏览器 UA(防 Cloudflare 风控)+ 代理强制 Static=true
(阻止远程 ffmpeg 转码调度,反代纯字节中继)
- 全局搜索改为按挂载逐个搜索(结果归属与伪装 ID 正确)
198 lines
6.3 KiB
Go
198 lines
6.3 KiB
Go
// Emby 挂载管理 HTTP 层:远程 Emby 服务器(账号)下的媒体库挂载 CRUD,
|
|
// 以及账号远程媒体库(View)列表预览。
|
|
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/ShukeBta/MMTL/internal/model"
|
|
"github.com/ShukeBta/MMTL/internal/service"
|
|
)
|
|
|
|
// embyMountView 挂载的对外 JSON(附带账号信息)。
|
|
type embyMountView struct {
|
|
model.EmbyMount
|
|
AccountName string `json:"account_name"`
|
|
}
|
|
|
|
// embyMountInput 创建挂载的请求体(单个或批量)。
|
|
type embyMountInput struct {
|
|
AccountID string `json:"account_id" binding:"required"`
|
|
Views []embyViewInput `json:"views" binding:"required,min=1"`
|
|
}
|
|
|
|
type embyViewInput struct {
|
|
RemoteViewID string `json:"remote_view_id" binding:"required"`
|
|
RemoteViewName string `json:"remote_view_name"`
|
|
CollectionType string `json:"collection_type"`
|
|
Name string `json:"name"`
|
|
ProxyPlay bool `json:"proxy_play"`
|
|
}
|
|
|
|
func embyMountViews(mounts []model.EmbyMount, accounts map[string]string) []embyMountView {
|
|
out := make([]embyMountView, 0, len(mounts))
|
|
for _, m := range mounts {
|
|
out = append(out, embyMountView{EmbyMount: m, AccountName: accounts[m.AccountID]})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// embyAccountViewsHandler 列出账号上的远程媒体库(View),供挂载选择。
|
|
func embyAccountViewsHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
acct := svc.EmbyRemote.AccountByID(c.Request.Context(), c.Param("id"))
|
|
if acct == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "账号不存在或已禁用"})
|
|
return
|
|
}
|
|
views, err := svc.EmbyRemote.RemoteViews(c.Request.Context(), acct)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
type viewEntry struct {
|
|
RemoteViewID string `json:"remote_view_id"`
|
|
RemoteViewName string `json:"remote_view_name"`
|
|
CollectionType string `json:"collection_type"`
|
|
ChildCount int `json:"child_count"`
|
|
AlreadyMounted bool `json:"already_mounted"`
|
|
}
|
|
mounted := map[string]bool{}
|
|
if mounts, err := svc.EmbyRemote.ListMountsByAccount(c.Request.Context(), acct.ID); err == nil {
|
|
for _, m := range mounts {
|
|
mounted[m.RemoteViewID] = true
|
|
}
|
|
}
|
|
out := make([]viewEntry, 0, len(views))
|
|
for _, v := range views {
|
|
viewID := service.RemoteItemIDString(v)
|
|
if strings.TrimSpace(viewID) == "" {
|
|
continue
|
|
}
|
|
out = append(out, viewEntry{
|
|
RemoteViewID: viewID,
|
|
RemoteViewName: service.RemoteItemNameString(v),
|
|
CollectionType: service.RemoteItemCollectionType(v),
|
|
ChildCount: service.RemoteItemChildCount(v),
|
|
AlreadyMounted: mounted[viewID],
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, out)
|
|
}
|
|
}
|
|
|
|
// listEmbyMountsHandler 列出全部挂载。
|
|
func listEmbyMountsHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
mounts, err := svc.EmbyRemote.ListMounts(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
names := map[string]string{}
|
|
if accounts, err := svc.EmbyRemote.ListAccounts(c.Request.Context()); err == nil {
|
|
for _, a := range accounts {
|
|
names[a.ID] = a.Name
|
|
}
|
|
}
|
|
out := embyMountViews(mounts, names)
|
|
if out == nil {
|
|
out = []embyMountView{}
|
|
}
|
|
c.JSON(http.StatusOK, out)
|
|
}
|
|
}
|
|
|
|
// createEmbyMountsHandler 批量创建挂载(同一账号下的多个远程媒体库)。
|
|
func createEmbyMountsHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req embyMountInput
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
mounts := make([]*model.EmbyMount, 0, len(req.Views))
|
|
for _, v := range req.Views {
|
|
mounts = append(mounts, &model.EmbyMount{
|
|
AccountID: req.AccountID,
|
|
RemoteViewID: v.RemoteViewID,
|
|
RemoteViewName: v.RemoteViewName,
|
|
CollectionType: v.CollectionType,
|
|
Name: v.Name,
|
|
ProxyPlay: v.ProxyPlay,
|
|
Enabled: true,
|
|
})
|
|
}
|
|
if _, err := svc.EmbyRemote.CreateMounts(c.Request.Context(), mounts); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true, "created": len(mounts)})
|
|
}
|
|
}
|
|
|
|
// fullMountEmbyAccountHandler 全量挂载:把账号所有远程媒体库一次挂载进来。
|
|
func fullMountEmbyAccountHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
acct := svc.EmbyRemote.AccountByID(c.Request.Context(), c.Param("id"))
|
|
if acct == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "账号不存在或已禁用"})
|
|
return
|
|
}
|
|
proxy := c.Query("proxy") == "1" || c.Query("proxy") == "true"
|
|
n, err := svc.EmbyRemote.FullMountAccount(c.Request.Context(), acct, proxy)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true, "created": n})
|
|
}
|
|
}
|
|
|
|
// updateEmbyMountHandler 更新挂载(显示名 / 代理开关 / 启用)。
|
|
func updateEmbyMountHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req struct {
|
|
Name *string `json:"name"`
|
|
ProxyPlay *bool `json:"proxy_play"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
mount, err := svc.EmbyRemote.MountByID(c.Request.Context(), c.Param("id"))
|
|
if err != nil || mount == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "挂载不存在"})
|
|
return
|
|
}
|
|
if req.Name != nil {
|
|
mount.Name = *req.Name
|
|
}
|
|
if req.ProxyPlay != nil {
|
|
mount.ProxyPlay = *req.ProxyPlay
|
|
}
|
|
if req.Enabled != nil {
|
|
mount.Enabled = *req.Enabled
|
|
}
|
|
if _, err := svc.EmbyRemote.UpdateMount(c.Request.Context(), mount.ID, mount); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, mount)
|
|
}
|
|
}
|
|
|
|
// deleteEmbyMountHandler 删除挂载。
|
|
func deleteEmbyMountHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if err := svc.EmbyRemote.DeleteMount(c.Request.Context(), c.Param("id")); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
} |