Rebrand MMTL to MeBox (name, logo, Docker image) (#17)

* 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>
This commit is contained in:
truewhile
2026-09-02 16:26:28 +08:00
committed by GitHub
parent a50ceaf1a7
commit b0fe40142a
435 changed files with 1152 additions and 1149 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/repository"
)
var (
@@ -11,8 +11,8 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestAdultProviderRouting(t *testing.T) {
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestNormalizeAdultCode(t *testing.T) {
+3 -3
View File
@@ -5,7 +5,7 @@
// read so values stored on disk are useless without the JWT secret.
//
// On first read it seeds the table with the providers supported by
// MMTL today (TMDb / Bangumi / TheTVDB / Fanart / OpenAI / Douban).
// MeBox today (TMDb / Bangumi / TheTVDB / Fanart / OpenAI / Douban).
package service
import (
@@ -17,8 +17,8 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// APIConfigService coordinates third-party API key storage.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// TestConnection 测试 API 连接。
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"net/url"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// GetEffectiveConfig 获取生效的 API 配置(数据库配置优先于配置文件)。
+3 -3
View File
@@ -7,9 +7,9 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// ApiConfigService 负责第三方 API 配置的 CRUD 和加密管理。
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// AuditService is the only sanctioned writer for the access_logs table.
+3 -3
View File
@@ -12,9 +12,9 @@ import (
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// AuthService handles registration, login, and JWT issuance.
+4 -4
View File
@@ -9,7 +9,7 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// IssueToken signs a JWT for the given user (60min validity, includes tier).
@@ -21,7 +21,7 @@ func (s *AuthService) IssueToken(u *model.User) (string, error) {
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(60 * time.Minute)),
Issuer: "mmtl",
Issuer: "mebox",
Subject: u.ID,
},
}
@@ -68,7 +68,7 @@ func (s *AuthService) IssueExternalPlaybackToken(u *model.User, mediaID string,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ExternalPlaybackTokenDurationForMedia(durationSec))),
Issuer: "mmtl",
Issuer: "mebox",
Subject: u.ID,
},
}
@@ -95,7 +95,7 @@ func (s *AuthService) IssueEmbyToken(u *model.User) (string, error) {
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(EmbyTokenDuration)),
Issuer: "mmtl",
Issuer: "mebox",
Subject: u.ID,
},
}
+4 -4
View File
@@ -12,10 +12,10 @@ import (
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/database"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/database"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func newAuthTestServices(t *testing.T) (*repository.Container, *AuthService, *ProfileService, *PermissionService) {
+2 -2
View File
@@ -19,7 +19,7 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// BackupService manages database backups.
@@ -72,7 +72,7 @@ func (b *BackupService) Create(ctx context.Context) (*BackupInfo, error) {
return nil, err
}
ts := time.Now().Format("20060102_150405")
dst := filepath.Join(dir, fmt.Sprintf("mmtl_%s.db", ts))
dst := filepath.Join(dir, fmt.Sprintf("mebox_%s.db", ts))
// VACUUM INTO creates a clean, non-WAL copy atomically.
if err := b.db.WithContext(ctx).Exec("VACUUM INTO ?", dst).Error; err != nil {
+2 -2
View File
@@ -23,7 +23,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// BangumiProvider talks to https://api.bgm.tv.
@@ -181,7 +181,7 @@ func (b *BangumiProvider) getJSON(ctx context.Context, u string, out any) error
if err != nil {
return err
}
req.Header.Set("User-Agent", "MMTL/0.1 (https://github.com/ShukeBta/MMTL)")
req.Header.Set("User-Agent", "MeBox/0.1 (https://github.com/truewhile/MeBox)")
if t := strings.TrimSpace(b.cfg.Secrets.BangumiToken); t != "" {
req.Header.Set("Authorization", "Bearer "+t)
}
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"strconv"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// Bot / 设备管控相关的设置键。全部存储在 settings 表,由管理员通过
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
"testing"
"github.com/ShukeBta/MMTL/internal/service/cloud115"
"github.com/truewhile/MeBox/internal/service/cloud115"
)
// 115 开放平台(openapi)驱动测试:mock proapi 的列目录/直链接口。
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// cloudDrive2Provider bridges CloudDrive2 through its WebDAV endpoint.
//
// CloudDrive2 integrates many cloud disks (115 / 123 / Aliyun and more).
// Treating it as a WebDAV-backed cloud provider lets MMTL
// Treating it as a WebDAV-backed cloud provider lets MeBox
// browse, mount and upload to those disks without carrying every provider's
// private chunk-upload protocol in this project.
type cloudDrive2Provider struct {
+3 -3
View File
@@ -21,7 +21,7 @@ import (
// embyProvider implements Provider against a remote Emby server using an
// api_key (token) for authentication. DirectLink.Resolve returns the remote
// stream URL; whether MMTL reverse-proxies the bytes is decided by the
// stream URL; whether MeBox reverse-proxies the bytes is decided by the
// emby.proxy_play account config (defaults to off).
type embyProvider struct {
base string // e.g. http://host:8096(自动补 /emby 前缀)
@@ -159,7 +159,7 @@ func (p *embyProvider) ensureToken(ctx context.Context) (string, error) {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Emby-Authorization", `MediaBrowser Client="MMTL", Device="MMTL-Federated", DeviceId="mmtl-federated", Version="1.0"`)
req.Header.Set("X-Emby-Authorization", `MediaBrowser Client="MeBox", Device="MeBox-Federated", DeviceId="mebox-federated", Version="1.0"`)
resp, err := p.client.Do(req)
if err != nil {
return "", err
@@ -234,7 +234,7 @@ func (p *embyProvider) List(ctx context.Context, dirID string) ([]FileEntry, err
}
// Resolve 返回远程 Emby 直链。Proxy=true 时由调用方(StrmService.ProxyDirect)
// 反向代理流量;false 时 302 到直链。默认不代理(播放字节不经过 MMTL)。
// 反向代理流量;false 时 302 到直链。默认不代理(播放字节不经过 MeBox)。
func (p *embyProvider) Resolve(ctx context.Context, fileRef string) (*DirectLink, error) {
token, err := p.ensureToken(ctx)
if err != nil {
+2 -2
View File
@@ -22,7 +22,7 @@ import (
"path/filepath"
"strings"
"github.com/ShukeBta/MMTL/internal/service/cloud115"
"github.com/truewhile/MeBox/internal/service/cloud115"
)
// OpenAPI115Provider 暴露 115 开放平台驱动接口。
@@ -112,7 +112,7 @@ func (p *openAPI115Provider) OpenClient() *cloud115.OpenClient { return p.c }
// io.Reader 无法携带文件名,因此走独立的 named 上传接口。将内容落为临时文件后
// 重命名为目标文件名,再交给 115 上传(/open/upload/init 的 file_name 取真实文件名)。
func (p *openAPI115Provider) PutFileNamed(ctx context.Context, parentCID, fileName string, r io.Reader) error {
tmp, err := os.CreateTemp("", "mmtl-upload-*")
tmp, err := os.CreateTemp("", "mebox-upload-*")
if err != nil {
return fmt.Errorf("115: 创建临时文件失败:%w", err)
}
+1 -1
View File
@@ -39,7 +39,7 @@ const (
)
// DefaultUA 请求 UA(标准 API 客户端格式,避免伪装浏览器触发 115 WAF 拦截与 405 错误)。
const DefaultUA = "MMTL-115-GoClient/1.0"
const DefaultUA = "MeBox-115-GoClient/1.0"
// OpenAPIError 保留 115 开放平台返回的原始错误信息。
type OpenAPIError struct {
@@ -6,8 +6,8 @@ import (
"path"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// CloudMountInfo 是云盘挂载库的规范标识(因网盘后端已移除,仅保留类型以兼容
+3 -3
View File
@@ -29,12 +29,12 @@ import (
// dandanplayObfuscationKey 是解开内置凭据的 XOR 混淆密钥。它与密文
// 同处一个二进制,仅作提取门槛,不作安全边界。
const dandanplayObfuscationKey = "MMTL-Danmaku#2026!v2"
const dandanplayObfuscationKey = "MeBox-Danmaku#2026!v2"
// 内置回退凭据(XOR 混淆后的 hex 编码)。
const (
danmakuEmbeddedAppIDHex = "352c24755f7c115d0a52"
danmakuEmbeddedAppKeyHex = "74390d751832375917150d4717586578586620652a05043d5e320f0d0b120d38"
danmakuEmbeddedAppIDHex = "350432560a153452095e"
danmakuEmbeddedAppKeyHex = "74111b564d5b1256141907594149677a5c717721550535331c0e4327071d0b2c"
)
// danmakuEmbeddedCredentials 解出内置回退凭据。
+3 -3
View File
@@ -16,8 +16,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/service/cloud"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/service/cloud"
)
// overrideDanmakuOfficialBase points the "official" endpoint (match + fallback)
@@ -34,7 +34,7 @@ func overrideDanmakuOfficialBase(t *testing.T, base string) {
// since the file is smaller than the 16MB prefix).
func writeDanmakuTestVideo(t *testing.T, name string) (path, wantHash string) {
t.Helper()
content := bytes.Repeat([]byte("MMTL-danmaku-hash-test-0123456789"), 500)
content := bytes.Repeat([]byte("MeBox-danmaku-hash-test-0123456789"), 500)
path = filepath.Join(t.TempDir(), name)
require.NoError(t, os.WriteFile(path, content, 0o644))
sum := md5.Sum(content)
+6 -6
View File
@@ -20,8 +20,8 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// Danmaku setting keys, managed through the admin settings UI (PUT
@@ -439,7 +439,7 @@ func (s *DanmakuService) fetchBody(ctx context.Context, sourceURL string, follow
if err != nil {
return "", err
}
req.Header.Set("User-Agent", "MMTL/danmaku (+https://github.com/ShukeBta/MMTL)")
req.Header.Set("User-Agent", "MeBox/danmaku (+https://github.com/truewhile/MeBox)")
req.Header.Set("Accept", "application/json, application/xml, */*")
if appID, appKey, ok := s.danmakuCredentials(ctx, sourceURL); ok {
// 签名认证:base64(sha256(AppId+Timestamp+Path+Secret)),密钥不出服务器。
@@ -522,7 +522,7 @@ func (s *DanmakuService) hashCachePut(stamp, hash string) {
}
// danmakuMatchFileName derives the /api/v2/match fileName: base name without
// the final extension. .strm items are covered too — MMTL strm files drop the
// the final extension. .strm items are covered too — MeBox strm files drop the
// video extension ("xxx.strm") while pre-existing ones may keep it
// ("xxx.mkv.strm") — so a second strip removes a real video extension only
// (filepath.Ext would misread names like "xxx.第01话" as having an extension).
@@ -636,7 +636,7 @@ func (s *DanmakuService) hashLocalFile(path string) (string, bool) {
}
// hashStrmTarget computes the video hash behind a .strm indirection:
// MMTL-internal /api/strm/play URLs are resolved through strmResolve (local
// MeBox-internal /api/strm/play URLs are resolved through strmResolve (local
// path read directly, cloud links range-fetched); plain http(s) links are
// fetched directly. Only the 16MB prefix is ever downloaded.
func (s *DanmakuService) hashStrmTarget(ctx context.Context, raw string) (string, bool) {
@@ -707,7 +707,7 @@ func (s *DanmakuService) openRangeBody(ctx context.Context, target string, heade
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "MMTL/danmaku (+https://github.com/ShukeBta/MMTL)")
req.Header.Set("User-Agent", "MeBox/danmaku (+https://github.com/truewhile/MeBox)")
req.Header.Set("Range", fmt.Sprintf("bytes=0-%d", danmakuHashPrefixBytes-1))
for k, v := range headers {
req.Header.Set(k, v)
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func newDanmakuTestService(t *testing.T) *DanmakuService {
+3 -3
View File
@@ -8,9 +8,9 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/database"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/database"
"github.com/truewhile/MeBox/internal/repository"
)
// DatabaseAdminService manages database configuration, connectivity testing, and migration.
+1 -1
View File
@@ -1,6 +1,6 @@
package service
import "github.com/ShukeBta/MMTL/internal/repository"
import "github.com/truewhile/MeBox/internal/repository"
func IsTransientDatabaseLock(err error) bool {
return repository.IsSQLiteBusyError(err)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// SweepInactiveUsers is kept for compatibility with the existing scheduler; it
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// KickDevice marks a device as kicked so the next request from it is rejected
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// DeviceService implements device/session tracking and the two decoupled
+1 -1
View File
@@ -6,7 +6,7 @@
// service to start playback of an HTTP URL.
//
// We do NOT mediate the renderer ↔ client traffic; the renderer pulls
// the bytes directly from MMTL's /api/stream endpoint, so
// the bytes directly from MeBox's /api/stream endpoint, so
// the cast call only ever transports a URL string.
package service
+1 -1
View File
@@ -27,7 +27,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// DoubanProvider talks to the unofficial Douban movie API.
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// ImageURL returns artwork for a media/series/season item id.
+1 -1
View File
@@ -3,7 +3,7 @@ package service
import (
"testing"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestImageURLFallsBackToLibraryCoverURL(t *testing.T) {
+4 -4
View File
@@ -3,7 +3,7 @@
// EmbyService produces JSON envelopes shaped like the most-consumed
// Emby-API endpoints so existing players (Infuse / Yamby / Hills /
// Senplayer / Kodi NextPVR extension / iOS native clients) can talk to
// MMTL without a custom plugin.
// MeBox without a custom plugin.
//
// The shim is read-mostly: items, images, playback are fully covered;
// 播放进度上报 / 收藏切换 是写路径但走我们自己的 PlaybackHistory /
@@ -17,14 +17,14 @@ import (
"sync"
"time"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/repository"
"go.uber.org/zap"
)
// 用一个固定的 ServerId 字符串。Emby 客户端会缓存这个 id,第一次见到
// 该 id 后会把所有派生数据(cookie/收藏/历史)和它绑定。
const embyServerID = "mmtl-001"
const embyServerID = "mebox-001"
// embyCompatVersion deliberately reports an Emby 4.x server. Official Emby
// clients reject Jellyfin-style 10.x identities as unsupported/too old during
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func newTestEmbyService(t *testing.T) *EmbyService {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
"gorm.io/gorm"
)
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// Item 单条目详情。
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func (e *EmbyService) mediaItems(ctx context.Context, p ItemsParams) (map[string]any, error) {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"sort"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func (e *EmbyService) mediaSourcesForItem(ctx context.Context, m *model.Media, asEmbedded, directOnly bool) []map[string]any {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// movieLibraryHasEpisodicContent 报告电影类型库里是否混入了「剧集结构」内容
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestEmbyMovieLibrarySeasonNumbersStayMovies(t *testing.T) {
+2 -2
View File
@@ -11,12 +11,12 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// PlaybackInfo returns a PlaybackInfoResponse usable by Emby clients.
// 远程 Emby 条目直接转发远程 PlaybackInfo,并按账号 proxy_play 配置决定
// 播放地址指向远程(直连)还是 MMTL 本地代理端点。
// 播放地址指向远程(直连)还是 MeBox 本地代理端点。
func (e *EmbyService) PlaybackInfo(ctx context.Context, mediaID, userID string) (map[string]any, error) {
if e.remote != nil && IsEmbyRemoteID(mediaID) {
mountID, remoteID, _ := DecodeEmbyRemoteID(mediaID)
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestEmbyRootItemsExposeLibraries(t *testing.T) {
+11 -11
View File
@@ -1,13 +1,13 @@
// EmbyRemoteService 是「远程 Emby 联邦聚合」核心:把挂载的远程 Emby 服务器
// 作为外部媒体源,通过 MMTL 的 Emby 兼容 API 透出。
// 作为外部媒体源,通过 MeBox 的 Emby 兼容 API 透出。
//
// 设计要点:
// - 远程媒体的元数据完全不落库:每次请求实时向远程 Emby 拉取;
// - 条目 ID 用 embyremote~{accountID}~{remoteID} 伪装(见 emby_remote_ids.go),
// 客户端拿伪装 ID 回来时按账号路由回远程;
// - 播放分流由账号级 proxy_play 配置决定:
// 不代理(默认)= MediaSource 下发热门远程绝对 URL,播放字节完全不经过 MMTL;
// 代理 = 下发 MMTL 本地 /Videos/{encoded} 端点,由 ProxyVideoStream 反向拉流。
// 不代理(默认)= MediaSource 下发热门远程绝对 URL,播放字节完全不经过 MeBox;
// 代理 = 下发 MeBox 本地 /Videos/{encoded} 端点,由 ProxyVideoStream 反向拉流。
//
// 配置复用 STRM 账号体系(StrmAccount.Provider = emby_remote),CRUD/加密/连通
// 测试全部走既有 /admin/strm/accounts 接口,不需要新增数据表。
@@ -30,9 +30,9 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// embyRemoteHTTPTimeout 远程 Emby 常规 API 请求超时(流式代理不在此列)。
@@ -63,7 +63,7 @@ type EmbyRemoteConfig struct {
Password string
Token string // api_key(手动填写或自动认证获得)
RemoteUserID string // 远程用户 Id(自动认证后回填)
ProxyPlay bool // true=播放流量经 MMTL 反向代理;false=客户端直连远程
ProxyPlay bool // true=播放流量经 MeBox 反向代理;false=客户端直连远程
}
// EmbyRemoteService 提供对远程 Emby 服务器的读写封装。
@@ -401,7 +401,7 @@ func (r *EmbyRemoteService) ensureTokenOnLine(ctx context.Context, acct *model.S
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Emby-Authorization", `MediaBrowser Client="MMTL", Device="MMTL-Federated", DeviceId="mmtl-federated", Version="1.0"`)
req.Header.Set("X-Emby-Authorization", `MediaBrowser Client="MeBox", Device="MeBox-Federated", DeviceId="mebox-federated", Version="1.0"`)
resp, err := r.http.Do(req)
if err != nil {
return fmt.Errorf("连接远程 Emby 失败: %w", err)
@@ -699,7 +699,7 @@ func (r *EmbyRemoteService) RemoteLatest(ctx context.Context, mount *model.EmbyM
}
// RemotePlaybackInfo 拉取远程 PlaybackInfo,并按挂载的 proxy_play 配置重写
// 播放 URL:不代理=指向远程绝对地址(播放字节不过 MMTL);代理=指向 MMTL
// 播放 URL:不代理=指向远程绝对地址(播放字节不过 MeBox);代理=指向 MeBox
// 本地 /Videos/{encodedID} 端点(由 ProxyVideoStream 反代)。
func (r *EmbyRemoteService) RemotePlaybackInfo(ctx context.Context, mount *model.EmbyMount, acct *model.StrmAccount, remoteID, userID string) (map[string]any, error) {
cfg, err := r.configOf(acct)
@@ -719,7 +719,7 @@ func (r *EmbyRemoteService) RemotePlaybackInfo(ctx context.Context, mount *model
// rewritePlayURLs 按挂载代理模式重写载荷内 MediaSources 的播放地址。
// 远程 Emby 的 PlaybackInfo 通常不返回 DirectStreamUrl(客户端靠它拼
// /Videos/{Id}/stream),因此这里总是强制构造播放地址,完全由 MMTL 掌控
// /Videos/{Id}/stream),因此这里总是强制构造播放地址,完全由 MeBox 掌控
// 直连(远程绝对 URL)或代理(本地 /Videos/{encoded})的最终去向。
func (r *EmbyRemoteService) rewritePlayURLs(value any, mount *model.EmbyMount, cfg *EmbyRemoteConfig, remoteID string) {
encoded := EncodeEmbyRemoteID(mount.ID, remoteID)
@@ -979,7 +979,7 @@ func (r *EmbyRemoteService) proxySubtitleOnLine(ctx context.Context, w http.Resp
// ─── 播放状态透传 ──────────────────────────────────────────────────────────────
// ProxySetPlayed 把「已看/未看」状态透传到远程 Emby(MMTL 本地不落库)。
// ProxySetPlayed 把「已看/未看」状态透传到远程 Emby(MeBox 本地不落库)。
func (r *EmbyRemoteService) ProxySetPlayed(ctx context.Context, acct *model.StrmAccount, remoteID string, played bool) error {
cfg, err := r.configOf(acct)
if err != nil {
+2 -2
View File
@@ -1,6 +1,6 @@
// 远程 Emby 聚合的 ID 伪装。
//
// MMTL 作为 Emby 联邦网关把多个远程 Emby 服务器的媒体库透明聚合到自身的
// MeBox 作为 Emby 联邦网关把多个远程 Emby 服务器的媒体库透明聚合到自身的
// Emby API 之下,远程条目完全不落库。为了把本地 ID 与多个远程服务器的 ID
// 隔离开,远程条目在返回给客户端之前统一被改写为:
//
@@ -42,7 +42,7 @@ func DecodeEmbyRemoteID(id string) (accountID, remoteID string, ok bool) {
}
// embyRemoteStringIDs 是条目 JSON 中需要伪装(编码)成远程 ID 的字符串字段。
// 图片 / 详情 / 播放请求都会以这些字段的值作为 ID 回指 MMTL。
// 图片 / 详情 / 播放请求都会以这些字段的值作为 ID 回指 MeBox。
var embyRemoteStringIDs = []string{
"Id",
"ParentId",
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"strconv"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// EmbyRemoteLine 表示同一 Emby 服务器的一条接入线路(内网/外网/CDN 等)。
+1 -1
View File
@@ -3,7 +3,7 @@ package service
import (
"testing"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// rewriteSubtitleDeliveryURLs 只应改动字幕轨道的 DeliveryUrl,其余媒体流不动。
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// RemoteLibraryView 是一个网页端可见的远程媒体库(对应一个挂载的远程 View)。
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestMapRemoteItemToMediaSortingFields(t *testing.T) {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
type embySeriesGroup struct {
@@ -6,7 +6,7 @@ import (
"testing"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestEmbyItemsExposeSeriesSeasonEpisodeHierarchy(t *testing.T) {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func (e *EmbyService) seriesIDForMedia(m *model.Media) string {
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
)
// writeTempVideoWithSubtitle creates a temp directory with a fake video and a
+4 -4
View File
@@ -5,7 +5,7 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// SystemInfo returns the full Emby identity payload.
@@ -13,7 +13,7 @@ func (e *EmbyService) SystemInfo() map[string]any {
return map[string]any{
"Id": embyServerID,
"ServerId": embyServerID,
"ServerName": "MMTL",
"ServerName": "MeBox",
"Version": embyCompatVersion,
"ServerVersion": embyCompatVersion,
"ProductName": "Emby Server",
@@ -42,7 +42,7 @@ func (e *EmbyService) SystemInfoPublic() map[string]any {
return map[string]any{
"Id": embyServerID,
"ServerId": embyServerID,
"ServerName": "MMTL",
"ServerName": "MeBox",
"Version": embyCompatVersion,
"ServerVersion": embyCompatVersion,
"ProductName": "Emby Server",
@@ -85,7 +85,7 @@ func (e *EmbyService) userPayload(u *model.User) map[string]any {
"Id": u.ID,
"Name": u.Username,
"ServerId": embyServerID,
"ServerName": "MMTL",
"ServerName": "MeBox",
"HasPassword": true,
"HasConfiguredPassword": true,
"HasConfiguredEasyPassword": false,
+3 -3
View File
@@ -9,7 +9,7 @@ import (
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// SetFavorite 把 mediaID 标为 userID 的收藏。远程 Emby 条目直接透传到对应
@@ -121,9 +121,9 @@ func (e *EmbyService) RecordProgress(ctx context.Context, userID, mediaID string
return err
}
// mergeRemoteUserData applies the current MMTL user's locally recorded playback
// mergeRemoteUserData applies the current MeBox user's locally recorded playback
// state to remote Emby payloads. Remote metadata remains authoritative unless the
// user has played the item through MMTL.
// user has played the item through MeBox.
func (e *EmbyService) mergeRemoteUserData(ctx context.Context, userID string, payload any) error {
if strings.TrimSpace(userID) == "" || payload == nil {
return nil
+1 -1
View File
@@ -3,7 +3,7 @@ package service
import (
"testing"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestMergedRemoteUserData(t *testing.T) {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestEmbyLatestItemsCollapsesMovieVersions(t *testing.T) {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func (e *EmbyService) applyUserMediaVisibility(ctx context.Context, q *gorm.DB, userID string) *gorm.DB {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// pollutedEpisodeCleanupSettingKey marks that the one-time normalization has run.
@@ -6,8 +6,8 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func newCleanupTestContainer(t *testing.T) (*Container, *gorm.DB) {
+2 -2
View File
@@ -21,7 +21,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// FanartProvider talks to https://webservice.fanart.tv.
@@ -133,7 +133,7 @@ func (f *FanartProvider) getJSON(ctx context.Context, u string, out any) error {
if err != nil {
return err
}
req.Header.Set("User-Agent", "MMTL/0.1")
req.Header.Set("User-Agent", "MeBox/0.1")
resp, err := f.client.Do(req)
if err != nil {
return err
+3 -3
View File
@@ -16,7 +16,7 @@ import (
"github.com/ulikunitz/xz"
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// ffmpegDownloadTarget 描述某个平台对应的官方构建下载源。
@@ -121,7 +121,7 @@ func installFFmpegTools(ctx context.Context, log *zap.Logger, cfg *config.Config
return "", "", fmt.Errorf("创建安装目录失败: %w", err)
}
tempDir, err := os.MkdirTemp("", "mmtl-ffmpeg-*")
tempDir, err := os.MkdirTemp("", "mebox-ffmpeg-*")
if err != nil {
return "", "", fmt.Errorf("创建临时目录失败: %w", err)
}
@@ -202,7 +202,7 @@ func downloadFFmpegFile(ctx context.Context, log *zap.Logger, url, dest string)
if err != nil {
return err
}
req.Header.Set("User-Agent", "MMTL/ffmpeg-installer ("+runtime.GOOS+"/"+runtime.GOARCH+")")
req.Header.Set("User-Agent", "MeBox/ffmpeg-installer ("+runtime.GOOS+"/"+runtime.GOARCH+")")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
+2 -2
View File
@@ -20,8 +20,8 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/repository"
)
// FFmpegToolsService 管理 ffmpeg/ffprobe 的自动下载安装状态(单飞:同一时间
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
func TestFFmpegTargetForPlatform(t *testing.T) {
+1 -1
View File
@@ -18,7 +18,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// FFprobeService wraps the external ffprobe binary.
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
func TestFFprobeServiceDefaultsToSingleConcurrentProbe(t *testing.T) {
+2 -2
View File
@@ -16,8 +16,8 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/repository"
)
// FileManagerService browses and mutates the server-side filesystem.
+2 -2
View File
@@ -67,8 +67,8 @@ func (s *FileManagerService) allowedRoots() (map[string]string, error) {
add("movies", s.cfg.Media.MoviesDir)
add("tv", s.cfg.Media.TVDir)
add("anime", s.cfg.Media.AnimeDir)
add("downloads", envOrDefault("MMTL_DOWNLOAD_CONTAINER_DIR", "/downloads"))
add("media", envOrDefault("MMTL_MEDIA_CONTAINER_DIR", "/media"))
add("downloads", envOrDefault("MEBOX_DOWNLOAD_CONTAINER_DIR", "/downloads"))
add("media", envOrDefault("MEBOX_MEDIA_CONTAINER_DIR", "/media"))
if s.repo != nil && s.repo.Setting != nil {
addSetting := func(label, key string) {
if value, err := s.repo.Setting.Get(context.Background(), key); err == nil {
+3 -3
View File
@@ -8,9 +8,9 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func newFileManagerTestServiceWithRepo(t *testing.T, root string) (*FileManagerService, *repository.Container) {
+2 -2
View File
@@ -2,7 +2,7 @@
//
// Some deployments cannot reach image.tmdb.org directly (GFW, internal-only
// networks). ImageProxy fronts a remote image URL so the browser only ever
// talks to the MMTL origin. The proxy:
// talks to the MeBox origin. The proxy:
//
// - validates the URL scheme is http/https,
// - streams bytes through with a small disk cache under cache/images,
@@ -20,7 +20,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// ImageProxy fetches and caches remote images on behalf of the browser.
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
func TestImageProxyDoesNotNegativeCacheFailedRemoteImageFetch(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
var testJPEG = []byte{
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"regexp"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
var availabilityNoiseRE = regexp.MustCompile(`(?i)(自动订阅|订阅|全集|合集|complete|batch|s\d{1,2}e\d{1,3}|season\s*\d+|s\d{1,2}|第\s*\d+\s*季|第\s*\d+\s*[集话話期]|\(\d{4}\)|\b\d{4}\b|2160p|1080p|720p|4k|uhd|bluray|blu-ray|web-?dl|hdtv|remux|x26[45]|h\.?26[45]|hevc|avc|hdr10?\+?|dovi|dv|atmos|aac|ddp?5\.1|truehd|flac)`)
+1 -1
View File
@@ -1,6 +1,6 @@
package service
import "github.com/ShukeBta/MMTL/internal/model"
import "github.com/truewhile/MeBox/internal/model"
func applyLocalMetadata(m *model.Media, local *LocalMetadata) {
applyLocalIdentityMetadata(m, local)
@@ -3,7 +3,7 @@ package service
import (
"testing"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestApplyLocalMetadataPreservesPathHintAsPending(t *testing.T) {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
type ManualScrapeRequest struct {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
type manualTMDbCandidate struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func (s *ScraperService) ManualSearch(ctx context.Context, media *model.Media, query, provider, mediaType string) ([]ExternalMediaResult, error) {
+3 -3
View File
@@ -13,9 +13,9 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestManualRequestMatchFallsBackToCandidatePayload(t *testing.T) {
+3 -3
View File
@@ -4,9 +4,9 @@ package service
import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// MediaService offers high-level CRUD over libraries and media items.
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"sort"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
type mediaListCacheValue struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"regexp"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
var (
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func (s *MediaService) attachLibraryMetadata(ctx context.Context, items []model.Media) {
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"gorm.io/gorm"
)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
type LibraryRootInput struct {
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestCreateLibraryWithRootsAppendsToExistingLogicalLibrary(t *testing.T) {
+3 -3
View File
@@ -7,9 +7,9 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestListLibrariesWithPreview(t *testing.T) {
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// ListMedia paginates media items inside a library.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
type MediaMetadataUpdate struct {
+3 -3
View File
@@ -5,9 +5,9 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestUpdateMediaMetadataMarksManualMatch(t *testing.T) {
+10 -10
View File
@@ -69,8 +69,8 @@ func embeddedContainerMarkerCandidates(input string) []string {
part string
container string
}{
{part: "/media", container: envOrDefault("MMTL_MEDIA_CONTAINER_DIR", "/media")},
{part: "/downloads", container: envOrDefault("MMTL_DOWNLOAD_CONTAINER_DIR", "/downloads")},
{part: "/media", container: envOrDefault("MEBOX_MEDIA_CONTAINER_DIR", "/media")},
{part: "/downloads", container: envOrDefault("MEBOX_DOWNLOAD_CONTAINER_DIR", "/downloads")},
} {
part := strings.TrimRight(marker.part, "/")
container := strings.TrimRight(filepath.ToSlash(marker.container), "/")
@@ -83,17 +83,17 @@ func embeddedContainerMarkerCandidates(input string) []string {
// describeUnresolvedLibraryPath 把「路径解析失败」变成可操作的诊断信息:列出尝试过的
// 候选路径与当前的宿主机→容器映射状态。旧库存的是宿主机路径,在新版/容器内常因为缺少
// MMTL_MEDIA_DIR 映射而扫不出媒体——这条诊断帮助用户直接定位到底哪一步断了。
// MEBOX_MEDIA_DIR 映射而扫不出媒体——这条诊断帮助用户直接定位到底哪一步断了。
// 仅用于日志与扫描错误提示,不改变解析逻辑本身。
func describeUnresolvedLibraryPath(rawPath string) string {
rawPath = strings.TrimSpace(rawPath)
candidates := mappedPathCandidates(rawPath)
mediaHost := strings.TrimSpace(os.Getenv("MMTL_MEDIA_DIR"))
mediaContainer := envOrDefault("MMTL_MEDIA_CONTAINER_DIR", "/media")
mediaHost := strings.TrimSpace(os.Getenv("MEBOX_MEDIA_DIR"))
mediaContainer := envOrDefault("MEBOX_MEDIA_CONTAINER_DIR", "/media")
var b strings.Builder
fmt.Fprintf(&b, "媒体库路径无法解析为可访问目录:%s(已尝试候选:%s)", rawPath, strings.Join(candidates, " | "))
if mediaHost == "" {
b.WriteString(";未配置 MMTL_MEDIA_DIR / MMTL_MEDIA_CONTAINER_DIR。若为 Docker 部署且此库为旧宿主机路径,请设置这两个变量把宿主机路径映射到容器内路径,并确认对应 volume 已挂载。")
b.WriteString(";未配置 MEBOX_MEDIA_DIR / MEBOX_MEDIA_CONTAINER_DIR。若为 Docker 部署且此库为旧宿主机路径,请设置这两个变量把宿主机路径映射到容器内路径,并确认对应 volume 已挂载。")
} else {
fmt.Fprintf(&b, ";当前映射 %s → %s。请确认该库路径位于此宿主机目录下,或补充对应的 volume 与路径映射。", mediaHost, mediaContainer)
}
@@ -204,8 +204,8 @@ func dockerVolumePathCandidates(path string) []string {
env string
container string
}{
{env: "MMTL_MEDIA_DIR", container: envOrDefault("MMTL_MEDIA_CONTAINER_DIR", "/media")},
{env: "MMTL_DOWNLOAD_DIR", container: envOrDefault("MMTL_DOWNLOAD_CONTAINER_DIR", "/downloads")},
{env: "MEBOX_MEDIA_DIR", container: envOrDefault("MEBOX_MEDIA_CONTAINER_DIR", "/media")},
{env: "MEBOX_DOWNLOAD_DIR", container: envOrDefault("MEBOX_DOWNLOAD_CONTAINER_DIR", "/downloads")},
} {
container := cleanPathForVolumeMapping(mapping.container)
if container == "." || container == "" || strings.HasPrefix(container, ".") {
@@ -247,8 +247,8 @@ func dockerVolumePathCandidates(path string) []string {
part string
container string
}{
{part: "/media", container: envOrDefault("MMTL_MEDIA_CONTAINER_DIR", "/media")},
{part: "/downloads", container: envOrDefault("MMTL_DOWNLOAD_CONTAINER_DIR", "/downloads")},
{part: "/media", container: envOrDefault("MEBOX_MEDIA_CONTAINER_DIR", "/media")},
{part: "/downloads", container: envOrDefault("MEBOX_DOWNLOAD_CONTAINER_DIR", "/downloads")},
} {
part := strings.TrimRight(marker.part, "/")
container := strings.TrimRight(filepath.ToSlash(marker.container), "/")
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
const maxRecycleBinRecords = 200
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestListRecycleBinPrunesOldRowsOverLimit(t *testing.T) {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func mediaReleaseSortTime(media model.Media) time.Time {
+2 -2
View File
@@ -3,8 +3,8 @@ package service
import (
"context"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// SearchMedia performs a simple LIKE search across titles.
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
type seriesCardsCacheValue struct {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"regexp"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
var episodicPathRE = regexp.MustCompile(`(?i)[\\/](?:电视剧|剧集|连续剧|短剧|国产剧|国剧|大陆剧|华语剧|国产电视剧|大陆电视剧|华语电视剧|欧美剧|欧美电视剧|美剧|英剧|日韩剧|日韩电视剧|日剧|韩剧|港剧|台剧|港台剧|泰剧|综艺|纪录片|儿童|动漫|番剧|国漫|日番|韩漫|美漫|欧美动漫|欧美动画|其他动漫|tv|series|shows?|season[\s._-]*\d|s\d{1,2}(?:[\s._-]|[\\/])|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake|特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇)[\\/]`)
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
type mediaSeriesKeyResolver struct {
+3 -3
View File
@@ -7,9 +7,9 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestListRecentSeriesCardsCountsAllEpisodesInSeries(t *testing.T) {
+15 -15
View File
@@ -14,8 +14,8 @@ func TestResolveAccessibleLibraryPathMapsConfiguredHostMediaDir(t *testing.T) {
if err := os.MkdirAll(containerLibrary, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("MMTL_MEDIA_DIR", hostRoot)
t.Setenv("MMTL_MEDIA_CONTAINER_DIR", containerRoot)
t.Setenv("MEBOX_MEDIA_DIR", hostRoot)
t.Setenv("MEBOX_MEDIA_CONTAINER_DIR", containerRoot)
got, err := resolveAccessibleLibraryPath(filepath.Join(hostRoot, "电视剧", "国产剧"))
if err != nil {
@@ -33,8 +33,8 @@ func TestResolveAccessibleLibraryPathMapsWindowsDriveBeforeLinuxAbs(t *testing.T
if err := os.MkdirAll(containerLibrary, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("MMTL_MEDIA_DIR", `Q:\media`)
t.Setenv("MMTL_MEDIA_CONTAINER_DIR", containerRoot)
t.Setenv("MEBOX_MEDIA_DIR", `Q:\media`)
t.Setenv("MEBOX_MEDIA_CONTAINER_DIR", containerRoot)
for _, input := range []string{
`Q:\media\电视剧\国产剧`,
@@ -61,8 +61,8 @@ func TestResolveAccessibleLibraryPathRecoversDockerPollutedWindowsDrive(t *testi
if err := os.MkdirAll(containerLibrary, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("MMTL_MEDIA_DIR", `F:\media`)
t.Setenv("MMTL_MEDIA_CONTAINER_DIR", containerRoot)
t.Setenv("MEBOX_MEDIA_DIR", `F:\media`)
t.Setenv("MEBOX_MEDIA_CONTAINER_DIR", containerRoot)
got, err := resolveAccessibleLibraryPath(`/app/F:\media\电视剧\国产剧`)
if err != nil {
@@ -95,7 +95,7 @@ func TestResolveAccessibleLibraryPathMapsRelativeDockerMediaMarker(t *testing.T)
if err := os.MkdirAll(containerLibrary, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("MMTL_MEDIA_CONTAINER_DIR", containerRoot)
t.Setenv("MEBOX_MEDIA_CONTAINER_DIR", containerRoot)
got, err := resolveAccessibleLibraryPath(filepath.Join("media", "电视剧", "国产剧"))
if err != nil {
@@ -116,7 +116,7 @@ func TestResolveAccessibleLibraryPathMapsEmbeddedHostMediaMarker(t *testing.T) {
if err := os.MkdirAll(containerLibrary, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("MMTL_MEDIA_CONTAINER_DIR", containerRoot)
t.Setenv("MEBOX_MEDIA_CONTAINER_DIR", containerRoot)
got, err := resolveAccessibleLibraryPath("/vol1/1000/Docker/moviepilot-v2/media/电视剧/国产剧")
if err != nil {
@@ -134,7 +134,7 @@ func TestResolveAccessibleMappedPathMapsEmbeddedHostDownloadMarker(t *testing.T)
if err := os.MkdirAll(containerItem, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("MMTL_DOWNLOAD_CONTAINER_DIR", containerDownloads)
t.Setenv("MEBOX_DOWNLOAD_CONTAINER_DIR", containerDownloads)
got, _, err := resolveAccessibleMappedPath("/vol1/1000/Docker/qbittorrent/downloads/国产剧")
if err != nil {
@@ -180,7 +180,7 @@ func TestMappedPathCandidatesMapWindowsDriveDownloadMarker(t *testing.T) {
root := t.TempDir()
containerDownloads := filepath.Join(root, "container", "downloads")
containerLibrary := filepath.Join(containerDownloads, "国产剧")
t.Setenv("MMTL_DOWNLOAD_CONTAINER_DIR", containerDownloads)
t.Setenv("MEBOX_DOWNLOAD_CONTAINER_DIR", containerDownloads)
want := filepath.Clean(containerLibrary)
for _, got := range mappedPathCandidates(`F:\downloads\国产剧`) {
@@ -197,8 +197,8 @@ func TestResolveMappedDestinationPathPrefersConfiguredContainerMapping(t *testin
if err := os.MkdirAll(containerMedia, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("MMTL_MEDIA_DIR", `Q:\media`)
t.Setenv("MMTL_MEDIA_CONTAINER_DIR", containerMedia)
t.Setenv("MEBOX_MEDIA_DIR", `Q:\media`)
t.Setenv("MEBOX_MEDIA_CONTAINER_DIR", containerMedia)
for _, input := range []string{`Q:\media`, `Q:/media`, `/app/Q:\media`} {
t.Run(input, func(t *testing.T) {
@@ -221,7 +221,7 @@ func TestResolveMappedDestinationPathPrefersContainerForRelativeMediaMarker(t *t
t.Fatal(err)
}
t.Chdir(workDir)
t.Setenv("MMTL_MEDIA_CONTAINER_DIR", containerMedia)
t.Setenv("MEBOX_MEDIA_CONTAINER_DIR", containerMedia)
got := resolveMappedDestinationPath(filepath.Join("media", "电视剧", "国产剧"))
want := filepath.Join(containerMedia, "电视剧", "国产剧")
@@ -237,8 +237,8 @@ func TestResolveAccessibleMappedPathMapsWindowsDownloadVariants(t *testing.T) {
if err := os.MkdirAll(containerSource, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("MMTL_DOWNLOAD_DIR", `Q:\downloads`)
t.Setenv("MMTL_DOWNLOAD_CONTAINER_DIR", containerDownloads)
t.Setenv("MEBOX_DOWNLOAD_DIR", `Q:\downloads`)
t.Setenv("MEBOX_DOWNLOAD_CONTAINER_DIR", containerDownloads)
for _, input := range []string{`Q:\downloads\国产剧`, `Q:/downloads/国产剧`, `/app/Q:\downloads\国产剧`} {
t.Run(input, func(t *testing.T) {

Some files were not shown because too many files have changed in this diff Show More