mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
b0fe40142a
* Rebrand MMTL to MeBox across codebase and assets Rename the project display name, Go module path, environment variable prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl to MeBox/mebox. Replace logo assets with the new MeBox icon and keep legacy SQLite migration support for existing mmtl.db deployments. Co-authored-by: truewhile <truewhile@users.noreply.github.com> * Fix logo icons: use cube-only crop without truncated text Previous icon generation cropped too much of the source image, including partial MeBox wordmark text that was cut off in square icon containers. Regenerate logo-64/192/512, favicon, and SVG from cube-only region. Co-authored-by: truewhile <truewhile@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
223 lines
7.0 KiB
Go
223 lines
7.0 KiB
Go
// Emby 挂载管理 HTTP 层:远程 Emby 服务器(账号)下的媒体库挂载 CRUD,
|
|
// 以及账号远程媒体库(View)列表预览。
|
|
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/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})
|
|
}
|
|
}
|
|
|
|
type reorderEmbyMountsReq struct {
|
|
IDs []string `json:"ids" binding:"required"`
|
|
}
|
|
|
|
// reorderEmbyMountsHandler 批量重排挂载媒体库顺序。
|
|
func reorderEmbyMountsHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req reorderEmbyMountsReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if svc.EmbyRemote == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "emby remote service not available"})
|
|
return
|
|
}
|
|
if err := svc.EmbyRemote.ReorderMounts(c.Request.Context(), req.IDs); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
}
|