mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
b0fe40142a
* 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>
259 lines
7.9 KiB
Go
259 lines
7.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/truewhile/MeBox/internal/config"
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
func TestEmbyLowercaseVideoStreamRouteServesMedia(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(model.AllModels()...); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
if err := repos.User.Create(t.Context(), &model.User{
|
|
Base: model.Base{ID: "user-1"},
|
|
Username: "tester",
|
|
PasswordHash: "x",
|
|
Role: "admin",
|
|
Tier: "plus",
|
|
IsActive: true,
|
|
}); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
dir := t.TempDir()
|
|
mediaPath := filepath.Join(dir, "sample.mp4")
|
|
if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil {
|
|
t.Fatalf("write media: %v", err)
|
|
}
|
|
lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatalf("create library: %v", err)
|
|
}
|
|
if err := db.Create(&model.Media{
|
|
Base: model.Base{ID: "media-1"},
|
|
LibraryID: lib.ID,
|
|
Title: "Lowercase Stream",
|
|
Path: mediaPath,
|
|
Container: "mp4",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create media: %v", err)
|
|
}
|
|
|
|
const secret = "test-secret"
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, secret, &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos),
|
|
Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil),
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/videos/media-1/stream?api_key="+signedTestToken(t, secret), nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if got := w.Body.String(); got != "fake-video-bytes" {
|
|
t.Fatalf("unexpected stream body: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEmbyPrefixedAPIStreamRouteServesMedia(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(model.AllModels()...); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
if err := repos.User.Create(t.Context(), &model.User{
|
|
Base: model.Base{ID: "user-1"},
|
|
Username: "tester",
|
|
PasswordHash: "x",
|
|
Role: "admin",
|
|
Tier: "plus",
|
|
IsActive: true,
|
|
}); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
dir := t.TempDir()
|
|
mediaPath := filepath.Join(dir, "sample.mp4")
|
|
if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil {
|
|
t.Fatalf("write media: %v", err)
|
|
}
|
|
lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatalf("create library: %v", err)
|
|
}
|
|
if err := db.Create(&model.Media{
|
|
Base: model.Base{ID: "media-1"},
|
|
LibraryID: lib.ID,
|
|
Title: "Prefixed API Stream",
|
|
Path: mediaPath,
|
|
Container: "mp4",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create media: %v", err)
|
|
}
|
|
|
|
const secret = "test-secret"
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, secret, &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos),
|
|
Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil),
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/emby/api/stream/media-1?api_key="+signedTestToken(t, secret), nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if got := w.Body.String(); got != "fake-video-bytes" {
|
|
t.Fatalf("unexpected stream body: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEmbyLowercaseOriginalHeadRouteServesHeaders(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(model.AllModels()...); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
if err := repos.User.Create(t.Context(), &model.User{
|
|
Base: model.Base{ID: "user-1"},
|
|
Username: "tester",
|
|
PasswordHash: "x",
|
|
Role: "admin",
|
|
Tier: "plus",
|
|
IsActive: true,
|
|
}); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
dir := t.TempDir()
|
|
mediaPath := filepath.Join(dir, "sample.mp4")
|
|
if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil {
|
|
t.Fatalf("write media: %v", err)
|
|
}
|
|
lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatalf("create library: %v", err)
|
|
}
|
|
if err := db.Create(&model.Media{
|
|
Base: model.Base{ID: "media-1"},
|
|
LibraryID: lib.ID,
|
|
Title: "Lowercase Original",
|
|
Path: mediaPath,
|
|
Container: "mp4",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create media: %v", err)
|
|
}
|
|
|
|
const secret = "test-secret"
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, secret, &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos),
|
|
Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil),
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodHead, "/videos/media-1/original.mp4?api_key="+signedTestToken(t, secret), nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if w.Body.Len() != 0 {
|
|
t.Fatalf("HEAD response should not include body, got %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestEmbyLowercaseVideoHLSRouteDoesNot404WhenDirectOnly(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(model.AllModels()...); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
if err := repos.User.Create(t.Context(), &model.User{
|
|
Base: model.Base{ID: "user-1"},
|
|
Username: "tester",
|
|
PasswordHash: "x",
|
|
Role: "admin",
|
|
Tier: "plus",
|
|
IsActive: true,
|
|
}); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
dir := t.TempDir()
|
|
mediaPath := filepath.Join(dir, "sample.mp4")
|
|
if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil {
|
|
t.Fatalf("write media: %v", err)
|
|
}
|
|
lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatalf("create library: %v", err)
|
|
}
|
|
if err := db.Create(&model.Media{
|
|
Base: model.Base{ID: "media-1"},
|
|
LibraryID: lib.ID,
|
|
Title: "Lowercase HLS",
|
|
Path: mediaPath,
|
|
Container: "mp4",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create media: %v", err)
|
|
}
|
|
if err := repos.Setting.Set(t.Context(), service.PlaybackDirectOnlySettingKey, "true"); err != nil {
|
|
t.Fatalf("set direct-only: %v", err)
|
|
}
|
|
|
|
const secret = "test-secret"
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, secret, &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos),
|
|
Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil),
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/videos/media-1/master.m3u8?api_key="+signedTestToken(t, secret), nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code == http.StatusNotFound {
|
|
t.Fatalf("lowercase HLS route should be registered, got 404")
|
|
}
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("direct-only HLS should return 409, got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|