mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-11 15:56:37 +08:00
feat(emby): support multiple connection lines for remote mount (#9)
Add account-level multi-line configuration for emby_remote mounts. Lines are stored in strm_accounts.config as a urls JSON array with backward compatibility for the legacy single url field. The backend automatically fails over to the next line on connection errors and persists the working active_line index. Frontend: multi-line editor on Emby mount page and STRM account dialog with add/remove/reorder, line names, and URL validation. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/model"
|
||||
)
|
||||
|
||||
// EmbyRemoteLine 表示同一 Emby 服务器的一条接入线路(内网/外网/CDN 等)。
|
||||
type EmbyRemoteLine struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func normalizeEmbyRemoteURL(raw string) string {
|
||||
u := strings.TrimRight(strings.TrimSpace(raw), "/")
|
||||
if u == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
||||
return ""
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// NormalizeEmbyRemoteLines 去掉空 URL,并规范每条线路地址。
|
||||
func NormalizeEmbyRemoteLines(lines []EmbyRemoteLine) []EmbyRemoteLine {
|
||||
out := make([]EmbyRemoteLine, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
u := normalizeEmbyRemoteURL(line.URL)
|
||||
if u == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, EmbyRemoteLine{
|
||||
Name: strings.TrimSpace(line.Name),
|
||||
URL: u,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseEmbyRemoteLines 从账号配置 JSON 中解析线路列表。
|
||||
// 兼容旧版单字段 url;urls 为 JSON 数组字符串。
|
||||
func ParseEmbyRemoteLines(raw map[string]string) ([]EmbyRemoteLine, int, error) {
|
||||
active := 0
|
||||
if v := strings.TrimSpace(raw["active_line"]); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
|
||||
active = n
|
||||
}
|
||||
}
|
||||
if urlsJSON := strings.TrimSpace(raw["urls"]); urlsJSON != "" {
|
||||
var lines []EmbyRemoteLine
|
||||
if err := json.Unmarshal([]byte(urlsJSON), &lines); err != nil {
|
||||
return nil, 0, fmt.Errorf("解析 Emby 线路列表: %w", err)
|
||||
}
|
||||
lines = NormalizeEmbyRemoteLines(lines)
|
||||
if len(lines) == 0 {
|
||||
return nil, 0, errors.New("至少配置一条 Emby 线路")
|
||||
}
|
||||
if active >= len(lines) {
|
||||
active = 0
|
||||
}
|
||||
return lines, active, nil
|
||||
}
|
||||
if u := normalizeEmbyRemoteURL(raw["url"]); u != "" {
|
||||
return []EmbyRemoteLine{{URL: u}}, 0, nil
|
||||
}
|
||||
return nil, 0, errors.New("缺少 Emby 地址")
|
||||
}
|
||||
|
||||
// EncodeEmbyRemoteLines 序列化线路列表,并返回兼容旧版的 primary URL。
|
||||
func EncodeEmbyRemoteLines(lines []EmbyRemoteLine) (urlsJSON string, primaryURL string, err error) {
|
||||
lines = NormalizeEmbyRemoteLines(lines)
|
||||
if len(lines) == 0 {
|
||||
return "", "", errors.New("至少配置一条 Emby 线路")
|
||||
}
|
||||
data, err := json.Marshal(lines)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return string(data), lines[0].URL, nil
|
||||
}
|
||||
|
||||
func (r *EmbyRemoteService) withLine(cfg *EmbyRemoteConfig, index int) *EmbyRemoteConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *cfg
|
||||
if index >= 0 && index < len(cfg.Lines) {
|
||||
copy.BaseURL = normalizeEmbyRemoteURL(cfg.Lines[index].URL)
|
||||
copy.ActiveLine = index
|
||||
}
|
||||
return ©
|
||||
}
|
||||
|
||||
func (r *EmbyRemoteService) lineOrder(cfg *EmbyRemoteConfig) []int {
|
||||
if cfg == nil || len(cfg.Lines) == 0 {
|
||||
return []int{0}
|
||||
}
|
||||
if len(cfg.Lines) == 1 {
|
||||
return []int{0}
|
||||
}
|
||||
active := cfg.ActiveLine
|
||||
if active < 0 || active >= len(cfg.Lines) {
|
||||
active = 0
|
||||
}
|
||||
order := []int{active}
|
||||
for i := range cfg.Lines {
|
||||
if i != active {
|
||||
order = append(order, i)
|
||||
}
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
func isEmbyLineFailoverError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
if strings.Contains(msg, "登录失败") ||
|
||||
strings.Contains(msg, "未返回 accesstoken") ||
|
||||
strings.Contains(msg, "缺少 emby 凭据") ||
|
||||
strings.Contains(msg, "认证重试失败") {
|
||||
return false
|
||||
}
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(msg, "连接远程 emby") ||
|
||||
strings.Contains(msg, "请求远程 emby 失败") ||
|
||||
strings.Contains(msg, "dial tcp") ||
|
||||
strings.Contains(msg, "connection refused") ||
|
||||
strings.Contains(msg, "no such host") ||
|
||||
strings.Contains(msg, "timeout") ||
|
||||
strings.Contains(msg, "i/o timeout")
|
||||
}
|
||||
|
||||
func (r *EmbyRemoteService) persistActiveLine(ctx context.Context, acct *model.StrmAccount, cfg *EmbyRemoteConfig, lineIndex int) error {
|
||||
if acct == nil || cfg == nil || lineIndex < 0 || lineIndex >= len(cfg.Lines) {
|
||||
return nil
|
||||
}
|
||||
raw := map[string]string{}
|
||||
if strings.TrimSpace(acct.Config) != "" {
|
||||
_ = json.Unmarshal([]byte(acct.Config), &raw)
|
||||
}
|
||||
raw["active_line"] = strconv.Itoa(lineIndex)
|
||||
raw["url"] = cfg.Lines[lineIndex].URL
|
||||
data, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
acct.Config = string(data)
|
||||
cfg.ActiveLine = lineIndex
|
||||
cfg.BaseURL = normalizeEmbyRemoteURL(cfg.Lines[lineIndex].URL)
|
||||
return r.repo.StrmAccount.Update(ctx, acct)
|
||||
}
|
||||
|
||||
func (r *EmbyRemoteService) adoptWorkingLine(ctx context.Context, acct *model.StrmAccount, cfg *EmbyRemoteConfig, lineIndex int) {
|
||||
if cfg == nil || lineIndex < 0 || lineIndex >= len(cfg.Lines) {
|
||||
return
|
||||
}
|
||||
if lineIndex == cfg.ActiveLine {
|
||||
return
|
||||
}
|
||||
_ = r.persistActiveLine(ctx, acct, cfg, lineIndex)
|
||||
}
|
||||
|
||||
// LinesOf 返回账号已配置的线路(供管理界面回显,不含敏感信息)。
|
||||
func (r *EmbyRemoteService) LinesOf(acct *model.StrmAccount) ([]EmbyRemoteLine, int, error) {
|
||||
raw := map[string]string{}
|
||||
if acct != nil && strings.TrimSpace(acct.Config) != "" {
|
||||
if err := json.Unmarshal([]byte(acct.Config), &raw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
return ParseEmbyRemoteLines(raw)
|
||||
}
|
||||
Reference in New Issue
Block a user