Files
MeBox/internal/handler/cloud.go
T
2026-06-09 20:22:00 +08:00

228 lines
6.7 KiB
Go

// Package handler — cloud-disk (网盘) endpoints: directory browsing, QR-code
// login, media import and 302 playback redirects.
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"
)
// cloudListHandler browses a configured cloud disk directory.
func cloudListHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
typ := c.Param("type")
dir := c.Query("dir")
entries, err := svc.StorageCfg.CloudList(c.Request.Context(), typ, dir)
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error(), "items": []any{}})
return
}
c.JSON(http.StatusOK, gin.H{"items": entries})
}
}
// cloudImportHandler turns a cloud file into a playable 302-backed media item.
func cloudImportHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
typ := c.Param("type")
var in struct {
Ref string `json:"ref" binding:"required"`
Name string `json:"name"`
Size int64 `json:"size"`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
m, err := svc.StorageCfg.CloudImport(c.Request.Context(), typ, in.Ref, in.Name, in.Size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, m)
}
}
// 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 网盘"
case cloud.TypeCloudDrive2:
base = "CloudDrive2"
}
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 {
return func(c *gin.Context) {
if c.Param("type") != cloud.Type115 {
c.JSON(http.StatusBadRequest, gin.H{"error": "qr login is only supported for 115"})
return
}
sess, err := cloud.QRStart(c.Request.Context(), nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, sess)
}
}
// cloud115QRPollHandler polls a 115 QR session; on confirmation it returns the
// session cookie so the frontend can save it as the storage credential.
func cloud115QRPollHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Param("type") != cloud.Type115 {
c.JSON(http.StatusBadRequest, gin.H{"error": "qr login is only supported for 115"})
return
}
var sess cloud.QRSession
if err := c.ShouldBindJSON(&sess); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
st, err := cloud.QRPoll(c.Request.Context(), nil, &sess)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, st)
}
}
// cloudPlayHandler resolves a cloud file to its direct link and either issues a
// 302 redirect (true offload — host does not stream the bytes) or, when the
// provider requires authenticated headers, reverse-proxies the response.
func cloudPlayHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
typ := c.Param("type")
ref := c.Query("ref")
if ref == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ref required"})
return
}
link, err := svc.StorageCfg.CloudResolve(c.Request.Context(), typ, ref, c.Request.UserAgent())
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
if !link.Proxy {
// Pure offload: send the client straight to the cloud CDN.
c.Redirect(http.StatusFound, link.URL)
return
}
// Proxy mode: the direct link needs auth headers the browser cannot
// carry. Stream through with Range forwarding.
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
}
for k, v := range link.Headers {
req.Header.Set(k, v)
}
if rng := c.GetHeader("Range"); rng != "" {
req.Header.Set("Range", rng)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
defer resp.Body.Close()
for _, h := range []string{"Content-Type", "Content-Length", "Content-Range", "Accept-Ranges"} {
if v := resp.Header.Get(h); v != "" {
c.Header(h, v)
}
}
c.Status(resp.StatusCode)
if c.Request.Method != http.MethodHead {
_, _ = io.Copy(c.Writer, resp.Body)
}
}
}