Files
MeBox/internal/service/emby_subtitle_test.go
truewhile b0fe40142a 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>
2026-09-02 16:26:28 +08:00

191 lines
6.8 KiB
Go

package service
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"go.uber.org/zap"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
)
// writeTempVideoWithSubtitle creates a temp directory with a fake video and a
// same-name external subtitle, and inserts the media row into the test DB. It
// returns the media id. The subtitle content is valid ASS for .ass/.ssa and
// valid SRT for .srt.
func writeTempVideoWithSubtitle(t *testing.T, svc *EmbyService, lib *model.Library, container, subExt string) string {
t.Helper()
dir := t.TempDir()
video := "MovieName" + container
sub := "MovieName" + subExt
if err := os.WriteFile(filepath.Join(dir, video), []byte("video"), 0o644); err != nil {
t.Fatalf("write video: %v", err)
}
var subBody string
switch strings.ToLower(subExt) {
case ".srt":
subBody = "1\n00:00:01,000 --> 00:00:02,000\nhello\n"
default:
subBody = "Dialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,hello\n"
}
if err := os.WriteFile(filepath.Join(dir, sub), []byte(subBody), 0o644); err != nil {
t.Fatalf("write subtitle: %v", err)
}
m := model.Media{
LibraryID: lib.ID,
Title: "MovieName",
Path: filepath.Join(dir, video),
Container: container,
VideoCodec: "h264",
AudioCodec: "aac",
}
if err := svc.repo.DB.Create(&m).Error; err != nil {
t.Fatalf("create media: %v", err)
}
return m.ID
}
func newTestSubtitleService(t *testing.T, svc *EmbyService) *SubtitleService {
t.Helper()
return NewSubtitleService(&config.Config{}, zap.NewNop(), svc.repo)
}
func TestEmbyMediaStreamsAttachSameNameSubtitle(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "电影", Path: `/media/movies`, Type: "movie", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
mediaID := writeTempVideoWithSubtitle(t, svc, &lib, ".mp4", ".ass")
svc.SetSubtitleService(newTestSubtitleService(t, svc))
m, err := svc.repo.Media.FindByID(t.Context(), mediaID)
if err != nil || m == nil {
t.Fatalf("find media: %v", err)
}
streams := svc.mediaStreams(t.Context(), m)
// Video (0) + Audio (1) + one Subtitle (2)
if len(streams) != 3 {
t.Fatalf("expected 3 streams (video/audio/subtitle), got %d: %#v", len(streams), streams)
}
sub := streams[2]
if sub["Type"] != "Subtitle" {
t.Fatalf("stream[2] type = %v, want Subtitle", sub["Type"])
}
if sub["Index"] != 2 {
t.Fatalf("subtitle index = %v, want 2", sub["Index"])
}
// Source codec (ASS) must be reported, and IsDefault must be false to match
// the official Emby external-subtitle contract.
if sub["Codec"] != "ass" {
t.Fatalf("subtitle codec = %v, want ass", sub["Codec"])
}
if sub["DeliveryFormat"] != "ass" {
t.Fatalf("subtitle delivery format = %v, want ass", sub["DeliveryFormat"])
}
if sub["IsDefault"] != false {
t.Fatalf("subtitle IsDefault = %v, want false", sub["IsDefault"])
}
if sub["DeliveryMethod"] != "External" {
t.Fatalf("subtitle DeliveryMethod = %v, want External", sub["DeliveryMethod"])
}
if got, _ := sub["Path"].(string); got == "" {
t.Fatalf("subtitle Path should not be empty: %#v", sub)
}
// Official Emby shape: /Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}
// (no "mediasource_" prefix; that prefix belongs to the Id value, not the route)
wantURL := "/Videos/" + mediaID + "/" + mediaID + "/Subtitles/2/Stream.ass"
if sub["DeliveryUrl"] != wantURL {
t.Fatalf("DeliveryUrl = %v, want %v", sub["DeliveryUrl"], wantURL)
}
if sub["IsTextSubtitleStream"] != true {
t.Fatalf("IsTextSubtitleStream = %v, want true", sub["IsTextSubtitleStream"])
}
if sub["SupportsExternalStream"] != true {
t.Fatalf("SupportsExternalStream = %v, want true", sub["SupportsExternalStream"])
}
}
func TestEmbyMediaStreamsSubtitleDisplayTitleFriendly(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "电影", Path: `/media/movies`, Type: "movie", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
mediaID := writeTempVideoWithSubtitle(t, svc, &lib, ".mp4", ".ass")
svc.SetSubtitleService(newTestSubtitleService(t, svc))
m, err := svc.repo.Media.FindByID(t.Context(), mediaID)
if err != nil || m == nil {
t.Fatalf("find media: %v", err)
}
streams := svc.mediaStreams(t.Context(), m)
sub := streams[2]
// No language tag on the filename -> should fall back to "字幕 (ASS)",
// matching official Emby's friendly label rather than a bare "und".
if sub["DisplayTitle"] != "字幕 (ASS)" {
t.Fatalf("DisplayTitle = %v, want 字幕 (ASS)", sub["DisplayTitle"])
}
}
func TestEmbyServeSubtitleStreamServesRawSource(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "电影", Path: `/media/movies`, Type: "movie", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
mediaID := writeTempVideoWithSubtitle(t, svc, &lib, ".mp4", ".ass")
svc.SetSubtitleService(newTestSubtitleService(t, svc))
var buf bytes.Buffer
if err := svc.ServeSubtitleStream(t.Context(), &buf, mediaID, "2", ""); err != nil {
t.Fatalf("serve subtitle: %v", err)
}
// Emby must get the RAW ASS source, NOT a WebVTT conversion.
if bytes.Contains(buf.Bytes(), []byte("WEBVTT")) {
t.Fatalf("served subtitle should be raw ASS, got WebVTT: %q", buf.String())
}
if !bytes.Contains(buf.Bytes(), []byte("Dialogue:")) {
t.Fatalf("served subtitle should contain raw ASS Dialogue lines: %q", buf.String())
}
}
func TestEmbyServeSubtitleStreamBadIndexNotFound(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "电影", Path: `/media/movies`, Type: "movie", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
mediaID := writeTempVideoWithSubtitle(t, svc, &lib, ".mp4", ".ass")
svc.SetSubtitleService(newTestSubtitleService(t, svc))
var buf bytes.Buffer
if err := svc.ServeSubtitleStream(t.Context(), &buf, mediaID, "99", ""); err != ErrSubtitleNotFound {
t.Fatalf("expected ErrSubtitleNotFound for index 99, got %v", err)
}
}
func TestEmbyMediaStreamsNoSubtitleServiceKeepsVideoAudio(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "电影", Path: `/media/movies`, Type: "movie", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
// Intentionally do NOT SetSubtitleService.
mediaID := writeTempVideoWithSubtitle(t, svc, &lib, ".mp4", ".srt")
m, err := svc.repo.Media.FindByID(t.Context(), mediaID)
if err != nil || m == nil {
t.Fatalf("find media: %v", err)
}
streams := svc.mediaStreams(t.Context(), m)
if len(streams) != 2 {
t.Fatalf("expected 2 streams (video/audio) without subtitle service, got %d: %#v", len(streams), streams)
}
}