mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
f20c9c3234
不再把 STRM/云盘媒体锁死为直连,解析远端源后允许 ffmpeg 转码播放。 Co-authored-by: Cursor <cursoragent@cursor.com>
116 lines
3.6 KiB
Go
116 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
)
|
|
|
|
// ServeFile streams the file backing the given media ID using
|
|
// http.ServeContent so HEAD / Range / If-Modified-Since are handled for free.
|
|
//
|
|
// When the media row has a STRMURL set we redirect (302) to that URL
|
|
// instead of opening a local file. This lets WebDAV / Alist / S3 / HTTP
|
|
// direct links flow through the rest of the player UI unchanged.
|
|
func (s *StreamService) ServeFile(w http.ResponseWriter, r *http.Request, mediaID string) error {
|
|
return s.ServeFileWithCloudMode(w, r, mediaID, "")
|
|
}
|
|
|
|
func (s *StreamService) ServeFileWithCloudMode(w http.ResponseWriter, r *http.Request, mediaID, cloudMode string) error {
|
|
m, err := s.repo.Media.FindByID(r.Context(), mediaID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if m == nil {
|
|
return ErrMediaNotFound
|
|
}
|
|
if strmURL := strings.TrimSpace(m.STRMURL); strmURL != "" && playableSTRMTarget(r.Context(), s.repo, strmURL, m) {
|
|
if !cloudPlaybackModeEnabled(r.Context(), s.repo, cloudMode) {
|
|
return ErrCloudPlaybackDisabled
|
|
}
|
|
// 云盘播放 URL 先规范化为相对路径,免疫扫描时固化的旧 host。
|
|
target := normalizeCloudPlayTarget(strmURL)
|
|
target = withAuthTokenForInternalRedirect(target, r, PublicServerURL(r.Context(), s.repo, s.cfg))
|
|
setCloudRedirectNoStore(w)
|
|
http.Redirect(w, r, absoluteInternalRedirect(target, r), http.StatusFound)
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(m.Path)), "cloud://") {
|
|
// 云盘媒体没有本地文件可回退;走到这里说明 STRM 播放被关闭或
|
|
// STRMURL 缺失。返回明确错误而不是笼统的「文件不存在」,
|
|
// 处理器据此回 502 + 原因,方便用户在播放器/日志里定位。
|
|
return ErrCloudPlaybackUnavailable
|
|
}
|
|
f, err := os.Open(m.Path)
|
|
if err != nil {
|
|
return ErrMediaNotFound
|
|
}
|
|
defer f.Close()
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.Header().Set("Accept-Ranges", "bytes")
|
|
w.Header().Set("Content-Disposition", "inline")
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
http.ServeContent(w, r, stat.Name(), stat.ModTime(), f)
|
|
return nil
|
|
}
|
|
|
|
func setCloudRedirectNoStore(w http.ResponseWriter) {
|
|
if w == nil {
|
|
return
|
|
}
|
|
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.Header().Set("Expires", "0")
|
|
}
|
|
|
|
func isCloudPlaybackTarget(raw string) bool {
|
|
_, _, ok := parseCloudMediaPlaybackURL(raw)
|
|
return ok
|
|
}
|
|
|
|
func playableSTRMTarget(ctx context.Context, repo *repository.Container, raw string, m *model.Media) bool {
|
|
if isCloudPlaybackTarget(raw) || isHTTPPlaybackTarget(raw) {
|
|
return true
|
|
}
|
|
if m != nil && strings.EqualFold(strings.TrimSpace(m.Container), "strm") {
|
|
return true
|
|
}
|
|
return STRMPlaybackEnabled(ctx, repo)
|
|
}
|
|
|
|
// IsStrmMediaRow 判断媒体行是否为 .strm(远程直链)媒体:STRMURL 非空、
|
|
// container=strm 或路径以 .strm 结尾。网页播放默认直连,失败后可转码。
|
|
func IsStrmMediaRow(m *model.Media) bool {
|
|
if m == nil {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(m.STRMURL) != "" {
|
|
return true
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(m.Container), "strm") {
|
|
return true
|
|
}
|
|
return strings.HasSuffix(strings.ToLower(strings.TrimSpace(m.Path)), ".strm")
|
|
}
|
|
|
|
func isStrmMediaRow(m *model.Media) bool {
|
|
return IsStrmMediaRow(m)
|
|
}
|
|
|
|
func isHTTPPlaybackTarget(raw string) bool {
|
|
u, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil || u == nil || !u.IsAbs() {
|
|
return false
|
|
}
|
|
scheme := strings.ToLower(strings.TrimSpace(u.Scheme))
|
|
return scheme == "http" || scheme == "https"
|
|
}
|