mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-26 10:30:46 +00:00
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
* refactor: rename core/transcode directory to core/stream * refactor: update all imports from core/transcode to core/stream * refactor: rename exported symbols to fit core/stream package name * refactor: simplify MediaStreamer interface to single NewStream method Remove the two-method interface (NewStream + DoStream) in favor of a single NewStream(ctx, mf, req) method. Callers are now responsible for fetching the MediaFile before calling NewStream. This removes the implicit DB lookup from the streamer, making it a pure streaming concern. * refactor: update all callers from DoStream to NewStream * chore: update wire_gen.go and stale comment for core/stream rename * refactor: update wire command to handle GO_BUILD_TAGS correctly Signed-off-by: Deluan <deluan@navidrome.org> * fix: distinguish not-found from internal errors in public stream handler * refactor: remove unused ID field from stream.Request * refactor: simplify ResolveRequestFromToken to receive *model.MediaFile Move MediaFile fetching responsibility to callers, making the method focused on token validation and request resolution. Remove ErrMediaNotFound (no longer produced). Update GetTranscodeStream handler to fetch the media file before calling ResolveRequestFromToken. * refactor: extend tokenTTL from 12 to 48 hours Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
237 lines
7.3 KiB
Go
237 lines
7.3 KiB
Go
package core_test
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/Masterminds/squirrel"
|
|
"github.com/navidrome/navidrome/core"
|
|
"github.com/navidrome/navidrome/core/stream"
|
|
"github.com/navidrome/navidrome/model"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
var _ = Describe("Archiver", func() {
|
|
var (
|
|
arch core.Archiver
|
|
ms *mockMediaStreamer
|
|
ds *mockDataStore
|
|
sh *mockShare
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
ms = &mockMediaStreamer{}
|
|
sh = &mockShare{}
|
|
ds = &mockDataStore{}
|
|
arch = core.NewArchiver(ms, ds, sh)
|
|
})
|
|
|
|
Context("ZipAlbum", func() {
|
|
It("zips an album correctly", func() {
|
|
mfs := model.MediaFiles{
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
|
|
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
|
|
}
|
|
|
|
mfRepo := &mockMediaFileRepository{}
|
|
mfRepo.On("GetAll", []model.QueryOptions{{
|
|
Filters: squirrel.Eq{"album_id": "1"},
|
|
Sort: "album",
|
|
}}).Return(mfs, nil)
|
|
|
|
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
|
|
|
|
out := new(bytes.Buffer)
|
|
err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)
|
|
Expect(err).To(BeNil())
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
Expect(err).To(BeNil())
|
|
|
|
Expect(len(zr.File)).To(Equal(2))
|
|
Expect(zr.File[0].Name).To(Equal("Album_Promo/01 - track1.mp3"))
|
|
Expect(zr.File[1].Name).To(Equal("Album_Promo/02 - track2.mp3"))
|
|
})
|
|
})
|
|
|
|
Context("ZipArtist", func() {
|
|
It("zips an artist's albums correctly", func() {
|
|
mfs := model.MediaFiles{
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumArtistID: "1", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
|
|
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumArtistID: "1", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
|
|
}
|
|
|
|
mfRepo := &mockMediaFileRepository{}
|
|
mfRepo.On("GetAll", []model.QueryOptions{{
|
|
Filters: squirrel.Eq{"album_artist_id": "1"},
|
|
Sort: "album",
|
|
}}).Return(mfs, nil)
|
|
|
|
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
|
|
|
out := new(bytes.Buffer)
|
|
err := arch.ZipArtist(context.Background(), "1", "mp3", 128, out)
|
|
Expect(err).To(BeNil())
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
Expect(err).To(BeNil())
|
|
|
|
Expect(len(zr.File)).To(Equal(2))
|
|
Expect(zr.File[0].Name).To(Equal("Album 1/01 - track1.mp3"))
|
|
Expect(zr.File[1].Name).To(Equal("Album 1/02 - track2.mp3"))
|
|
})
|
|
})
|
|
|
|
Context("ZipShare", func() {
|
|
It("zips a share correctly", func() {
|
|
mfs := model.MediaFiles{
|
|
{ID: "1", Path: "test_data/01 - track1.mp3", Suffix: "mp3", Artist: "Artist 1", Title: "track1"},
|
|
{ID: "2", Path: "test_data/02 - track2.mp3", Suffix: "mp3", Artist: "Artist 2", Title: "track2"},
|
|
}
|
|
|
|
share := &model.Share{
|
|
ID: "1",
|
|
Downloadable: true,
|
|
Format: "mp3",
|
|
MaxBitRate: 128,
|
|
Tracks: mfs,
|
|
}
|
|
|
|
sh.On("Load", mock.Anything, "1").Return(share, nil)
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
|
|
|
out := new(bytes.Buffer)
|
|
err := arch.ZipShare(context.Background(), "1", out)
|
|
Expect(err).To(BeNil())
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
Expect(err).To(BeNil())
|
|
|
|
Expect(len(zr.File)).To(Equal(2))
|
|
Expect(zr.File[0].Name).To(Equal("01 - Artist 1 - track1.mp3"))
|
|
Expect(zr.File[1].Name).To(Equal("02 - Artist 2 - track2.mp3"))
|
|
|
|
})
|
|
})
|
|
|
|
Context("ZipPlaylist", func() {
|
|
It("zips a playlist correctly", func() {
|
|
tracks := []model.PlaylistTrack{
|
|
{MediaFile: model.MediaFile{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "AC/DC", Title: "track1"}},
|
|
{MediaFile: model.MediaFile{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "Artist 2", Title: "track2"}},
|
|
}
|
|
|
|
pls := &model.Playlist{
|
|
ID: "1",
|
|
Name: "Test Playlist",
|
|
Tracks: tracks,
|
|
}
|
|
|
|
plRepo := &mockPlaylistRepository{}
|
|
plRepo.On("GetWithTracks", "1", true, false).Return(pls, nil)
|
|
ds.On("Playlist", mock.Anything).Return(plRepo)
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
|
|
|
out := new(bytes.Buffer)
|
|
err := arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out)
|
|
Expect(err).To(BeNil())
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
Expect(err).To(BeNil())
|
|
|
|
Expect(len(zr.File)).To(Equal(3))
|
|
Expect(zr.File[0].Name).To(Equal("01 - AC_DC - track1.mp3"))
|
|
Expect(zr.File[1].Name).To(Equal("02 - Artist 2 - track2.mp3"))
|
|
Expect(zr.File[2].Name).To(Equal("Test Playlist.m3u"))
|
|
|
|
// Verify M3U content
|
|
m3uFile, err := zr.File[2].Open()
|
|
Expect(err).To(BeNil())
|
|
defer m3uFile.Close()
|
|
|
|
m3uContent, err := io.ReadAll(m3uFile)
|
|
Expect(err).To(BeNil())
|
|
|
|
expectedM3U := "#EXTM3U\n#PLAYLIST:Test Playlist\n#EXTINF:0,AC/DC - track1\n01 - AC_DC - track1.mp3\n#EXTINF:0,Artist 2 - track2\n02 - Artist 2 - track2.mp3\n"
|
|
Expect(string(m3uContent)).To(Equal(expectedM3U))
|
|
})
|
|
})
|
|
})
|
|
|
|
type mockDataStore struct {
|
|
mock.Mock
|
|
model.DataStore
|
|
}
|
|
|
|
func (m *mockDataStore) MediaFile(ctx context.Context) model.MediaFileRepository {
|
|
args := m.Called(ctx)
|
|
return args.Get(0).(model.MediaFileRepository)
|
|
}
|
|
|
|
func (m *mockDataStore) Playlist(ctx context.Context) model.PlaylistRepository {
|
|
args := m.Called(ctx)
|
|
return args.Get(0).(model.PlaylistRepository)
|
|
}
|
|
|
|
func (m *mockDataStore) Library(context.Context) model.LibraryRepository {
|
|
return &mockLibraryRepository{}
|
|
}
|
|
|
|
type mockLibraryRepository struct {
|
|
mock.Mock
|
|
model.LibraryRepository
|
|
}
|
|
|
|
func (m *mockLibraryRepository) GetPath(id int) (string, error) {
|
|
return "/music", nil
|
|
}
|
|
|
|
type mockMediaFileRepository struct {
|
|
mock.Mock
|
|
model.MediaFileRepository
|
|
}
|
|
|
|
func (m *mockMediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
|
args := m.Called(options)
|
|
return args.Get(0).(model.MediaFiles), args.Error(1)
|
|
}
|
|
|
|
type mockPlaylistRepository struct {
|
|
mock.Mock
|
|
model.PlaylistRepository
|
|
}
|
|
|
|
func (m *mockPlaylistRepository) GetWithTracks(id string, refreshSmartPlaylists, includeMissing bool) (*model.Playlist, error) {
|
|
args := m.Called(id, refreshSmartPlaylists, includeMissing)
|
|
return args.Get(0).(*model.Playlist), args.Error(1)
|
|
}
|
|
|
|
type mockMediaStreamer struct {
|
|
mock.Mock
|
|
stream.MediaStreamer
|
|
}
|
|
|
|
func (m *mockMediaStreamer) NewStream(ctx context.Context, mf *model.MediaFile, req stream.Request) (*stream.Stream, error) {
|
|
args := m.Called(ctx, mf, req)
|
|
if args.Error(1) != nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return &stream.Stream{ReadCloser: args.Get(0).(io.ReadCloser)}, nil
|
|
}
|
|
|
|
type mockShare struct {
|
|
mock.Mock
|
|
core.Share
|
|
}
|
|
|
|
func (m *mockShare) Load(ctx context.Context, id string) (*model.Share, error) {
|
|
args := m.Called(ctx, id)
|
|
return args.Get(0).(*model.Share), args.Error(1)
|
|
}
|