mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
feat: add cloud transfer and cache optimizations
This commit is contained in:
@@ -5,9 +5,12 @@ package handler
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service/cloud"
|
||||
)
|
||||
@@ -48,6 +51,84 @@ func cloudImportHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// cloudMountHandler creates or reuses a cloud:// media library for a cloud
|
||||
// directory, then scans it recursively so cloud files become playable STRM/302
|
||||
// media rows without copying bytes to local disk.
|
||||
func cloudMountHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
typ := c.Param("type")
|
||||
var in struct {
|
||||
Dir string `json:"dir"`
|
||||
Name string `json:"name"`
|
||||
MediaType string `json:"media_type"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&in)
|
||||
if !cloud.IsCloudType(typ) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported cloud provider"})
|
||||
return
|
||||
}
|
||||
if _, err := svc.StorageCfg.CloudProvider(c.Request.Context(), typ); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
path := "cloud://" + typ
|
||||
if dir := strings.TrimSpace(in.Dir); dir != "" {
|
||||
path += "/" + url.PathEscape(dir)
|
||||
}
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
name = cloudMountLibraryName(typ, strings.TrimSpace(in.Dir))
|
||||
}
|
||||
mediaType := strings.TrimSpace(in.MediaType)
|
||||
if mediaType == "" {
|
||||
mediaType = "movie"
|
||||
}
|
||||
libs, err := svc.Repo.Library.List(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var lib *model.Library
|
||||
for i := range libs {
|
||||
if libs[i].Path == path {
|
||||
lib = &libs[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if lib == nil {
|
||||
lib = &model.Library{Name: name, Path: path, Type: mediaType, Enabled: true}
|
||||
if err := svc.Repo.Library.Create(c.Request.Context(), lib); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
var scan any
|
||||
if svc.Scan != nil {
|
||||
res, err := svc.Scan.ScanLibrary(c.Request.Context(), lib.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error(), "library": lib})
|
||||
return
|
||||
}
|
||||
scan = res
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"library": lib, "scan": scan})
|
||||
}
|
||||
}
|
||||
|
||||
func cloudMountLibraryName(typ, dir string) string {
|
||||
base := typ
|
||||
switch typ {
|
||||
case cloud.TypeQuark:
|
||||
base = "夸克网盘"
|
||||
case cloud.Type115:
|
||||
base = "115 网盘"
|
||||
}
|
||||
if dir == "" || dir == "0" {
|
||||
return base
|
||||
}
|
||||
return base + " · " + dir
|
||||
}
|
||||
|
||||
// cloud115QRStartHandler begins a 115 QR-code login and returns the session +
|
||||
// QR image URL for the frontend to render.
|
||||
func cloud115QRStartHandler(svc *service.Container) gin.HandlerFunc {
|
||||
@@ -102,7 +183,11 @@ func cloudPlayHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
// Proxy mode: the direct link needs auth headers the browser cannot
|
||||
// carry. Stream through with Range forwarding.
|
||||
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, link.URL, nil)
|
||||
method := c.Request.Method
|
||||
if method == "" {
|
||||
method = http.MethodGet
|
||||
}
|
||||
req, err := http.NewRequestWithContext(c.Request.Context(), method, link.URL, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -125,6 +210,8 @@ func cloudPlayHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
c.Status(resp.StatusCode)
|
||||
_, _ = io.Copy(c.Writer, resp.Body)
|
||||
if c.Request.Method != http.MethodHead {
|
||||
_, _ = io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,12 @@ func registerAdminRoutes(api *gin.RouterGroup, cfg *config.Config, svc *service.
|
||||
admin.GET("/storage/:type", getStorageConfigHandler(svc))
|
||||
admin.PUT("/storage/:type", saveStorageConfigHandler(svc))
|
||||
admin.POST("/storage/:type/test", testStorageConfigHandler(svc))
|
||||
admin.POST("/storage/:type/upload-local", storageUploadLocalHandler(svc))
|
||||
|
||||
// Cloud disk (115 / 夸克) browsing, QR login and 302 import.
|
||||
admin.GET("/cloud/:type/list", cloudListHandler(svc))
|
||||
admin.POST("/cloud/:type/import", cloudImportHandler(svc))
|
||||
admin.POST("/cloud/:type/mount", cloudMountHandler(svc))
|
||||
admin.POST("/cloud/:type/qr/start", cloud115QRStartHandler(svc))
|
||||
admin.POST("/cloud/:type/qr/poll", cloud115QRPollHandler(svc))
|
||||
|
||||
|
||||
@@ -73,3 +73,24 @@ func testStorageConfigHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
}
|
||||
|
||||
func storageUploadLocalHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req service.CloudUploadInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Type = c.Param("type")
|
||||
res, err := svc.StorageCfg.UploadLocal(c.Request.Context(), req)
|
||||
if err != nil && res == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"result": res, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"result": res})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,20 @@ func schemaHandler(_ *service.Container) gin.HandlerFunc {
|
||||
{"key": "scrape.delay_max_ms", "type": "number", "label": "刮削最大间隔毫秒"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "cloud-upload",
|
||||
"label": "网盘转存",
|
||||
"items": []gin.H{
|
||||
{"key": "cloud.upload_auto_enabled", "type": "toggle", "label": "启用自动转存"},
|
||||
{"key": "cloud.upload_provider", "type": "select", "label": "转存目标"},
|
||||
{"key": "cloud.upload_source_dir", "type": "text", "label": "本地源目录"},
|
||||
{"key": "cloud.upload_dest_path", "type": "text", "label": "网盘目标目录"},
|
||||
{"key": "cloud.upload_recursive", "type": "toggle", "label": "递归扫描源目录"},
|
||||
{"key": "cloud.upload_sidecars", "type": "toggle", "label": "同步 NFO / 海报 / 字幕"},
|
||||
{"key": "cloud.upload_overwrite", "type": "toggle", "label": "覆盖远端同名文件"},
|
||||
{"key": "cloud.upload_interval_seconds", "type": "number", "label": "自动转存间隔秒数"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "adult",
|
||||
"label": "Adult / NSFW",
|
||||
|
||||
Reference in New Issue
Block a user