From 52285a1c48d24ce89a0bd37c057a119e1144503a Mon Sep 17 00:00:00 2001 From: Phan An Date: Sat, 24 Feb 2024 14:28:49 +0700 Subject: [PATCH] refactor: use adapters for streamers --- .../UnsupportedSongStorageTypeException.php | 18 +++ app/Http/Controllers/PlayController.php | 19 ++- app/Providers/StreamerServiceProvider.php | 16 +-- .../Adapters/DropboxStreamerAdapter.php | 20 +++ .../Adapters/LocalStreamerAdapter.php | 7 + .../Adapters/PhpStreamerAdapter.php} | 10 +- .../Adapters/S3CompatibleStreamerAdapter.php | 20 +++ .../Streamer/Adapters/StreamerAdapter.php | 10 ++ .../Adapters/TranscodingStreamerAdapter.php | 45 ++++++ .../XAccelRedirectStreamerAdapter.php} | 16 ++- .../Adapters/XSendFileStreamerAdapter.php | 23 ++++ app/Services/Streamer/Streamer.php | 71 ++++++++++ app/Services/Streamers/DropboxStreamer.php | 20 --- app/Services/Streamers/LocalStreamer.php | 22 --- .../Streamers/S3CompatibleStreamer.php | 24 ---- app/Services/Streamers/Streamer.php | 24 ---- app/Services/Streamers/StreamerFactory.php | 50 ------- .../Streamers/TranscodingStreamer.php | 62 --------- app/Services/Streamers/XSendFileStreamer.php | 18 --- app/Services/TranscodingService.php | 13 -- ...> 1__01hqd2pnm2y0wvsyx5cerwh0k1__song.mp3} | Bin tests/Feature/KoelPlus/SongPlayTest.php | 23 +--- tests/Feature/SongPlayTest.php | 59 ++++++-- .../Factories/StreamerFactoryTest.php | 130 ------------------ .../Services/Streamer/StreamerTest.php | 47 +++++++ .../KoelPlus/StreamerFactoryTest.php | 57 -------- .../Services/Streamer/StreamerTest.php | 97 +++++++++++++ 27 files changed, 447 insertions(+), 474 deletions(-) create mode 100644 app/Exceptions/UnsupportedSongStorageTypeException.php create mode 100644 app/Services/Streamer/Adapters/DropboxStreamerAdapter.php create mode 100644 app/Services/Streamer/Adapters/LocalStreamerAdapter.php rename app/Services/{Streamers/PhpStreamer.php => Streamer/Adapters/PhpStreamerAdapter.php} (82%) create mode 100644 app/Services/Streamer/Adapters/S3CompatibleStreamerAdapter.php create mode 100644 app/Services/Streamer/Adapters/StreamerAdapter.php create mode 100644 app/Services/Streamer/Adapters/TranscodingStreamerAdapter.php rename app/Services/{Streamers/XAccelRedirectStreamer.php => Streamer/Adapters/XAccelRedirectStreamerAdapter.php} (55%) create mode 100644 app/Services/Streamer/Adapters/XSendFileStreamerAdapter.php create mode 100644 app/Services/Streamer/Streamer.php delete mode 100644 app/Services/Streamers/DropboxStreamer.php delete mode 100644 app/Services/Streamers/LocalStreamer.php delete mode 100644 app/Services/Streamers/S3CompatibleStreamer.php delete mode 100644 app/Services/Streamers/Streamer.php delete mode 100644 app/Services/Streamers/StreamerFactory.php delete mode 100644 app/Services/Streamers/TranscodingStreamer.php delete mode 100644 app/Services/Streamers/XSendFileStreamer.php delete mode 100644 app/Services/TranscodingService.php rename storage/framework/testing/disks/s3/{1__01hqcp4z8be6jf58jzdnsr2y0j__song.mp3 => 1__01hqd2pnm2y0wvsyx5cerwh0k1__song.mp3} (100%) delete mode 100644 tests/Integration/Factories/StreamerFactoryTest.php create mode 100644 tests/Integration/KoelPlus/Services/Streamer/StreamerTest.php delete mode 100644 tests/Integration/KoelPlus/StreamerFactoryTest.php create mode 100644 tests/Integration/Services/Streamer/StreamerTest.php diff --git a/app/Exceptions/UnsupportedSongStorageTypeException.php b/app/Exceptions/UnsupportedSongStorageTypeException.php new file mode 100644 index 00000000..9ee7e3e2 --- /dev/null +++ b/app/Exceptions/UnsupportedSongStorageTypeException.php @@ -0,0 +1,18 @@ +authorize('access', $song); - return $streamerFactory - ->createStreamer($song, $transcode, $bitRate, (float) $request->time) - ->stream(); + return (new Streamer(song: $song, config: [ + 'transcode' => (bool) $transcode, + 'bit_rate' => $bitRate, + 'start_time' => (float) $request->time, + ]))->stream(); } } diff --git a/app/Providers/StreamerServiceProvider.php b/app/Providers/StreamerServiceProvider.php index 96daec85..366346f4 100644 --- a/app/Providers/StreamerServiceProvider.php +++ b/app/Providers/StreamerServiceProvider.php @@ -2,21 +2,21 @@ namespace App\Providers; -use App\Services\Streamers\LocalStreamer; -use App\Services\Streamers\PhpStreamer; -use App\Services\Streamers\XAccelRedirectStreamer; -use App\Services\Streamers\XSendFileStreamer; +use App\Services\Streamer\Adapters\LocalStreamerAdapter; +use App\Services\Streamer\Adapters\PhpStreamerAdapter; +use App\Services\Streamer\Adapters\XAccelRedirectStreamerAdapter; +use App\Services\Streamer\Adapters\XSendFileStreamerAdapter; use Illuminate\Support\ServiceProvider; class StreamerServiceProvider extends ServiceProvider { public function register(): void { - $this->app->bind(LocalStreamer::class, static function (): LocalStreamer { + $this->app->bind(LocalStreamerAdapter::class, function (): LocalStreamerAdapter { return match (config('koel.streaming.method')) { - 'x-sendfile' => new XSendFileStreamer(), - 'x-accel-redirect' => new XAccelRedirectStreamer(), - default => new PhpStreamer(), + 'x-sendfile' => $this->app->make(XSendFileStreamerAdapter::class), + 'x-accel-redirect' => $this->app->make(XAccelRedirectStreamerAdapter::class), + default => $this->app->make(PhpStreamerAdapter::class), }; }); } diff --git a/app/Services/Streamer/Adapters/DropboxStreamerAdapter.php b/app/Services/Streamer/Adapters/DropboxStreamerAdapter.php new file mode 100644 index 00000000..dfee6171 --- /dev/null +++ b/app/Services/Streamer/Adapters/DropboxStreamerAdapter.php @@ -0,0 +1,20 @@ +storage->getSongPresignedUrl($song)); + } +} diff --git a/app/Services/Streamer/Adapters/LocalStreamerAdapter.php b/app/Services/Streamer/Adapters/LocalStreamerAdapter.php new file mode 100644 index 00000000..eefca944 --- /dev/null +++ b/app/Services/Streamer/Adapters/LocalStreamerAdapter.php @@ -0,0 +1,7 @@ +storage_metadata->getPath(); $rangeHeader = get_request_header('Range'); // On Safari, "Range" header value can be "bytes=0-1" which breaks streaming. $rangeHeader = $rangeHeader === 'bytes=0-1' ? 'bytes=0-' : $rangeHeader; $rangeSet = RangeSet::createFromHeader($rangeHeader); - $resource = new FileResource($this->song->path, mime_content_type($this->song->path)); + $resource = new FileResource($path, mime_content_type($path)); (new ResourceServlet($resource))->sendResource($rangeSet); } catch (InvalidRangeHeaderException) { abort(Response::HTTP_BAD_REQUEST); diff --git a/app/Services/Streamer/Adapters/S3CompatibleStreamerAdapter.php b/app/Services/Streamer/Adapters/S3CompatibleStreamerAdapter.php new file mode 100644 index 00000000..e84e6d08 --- /dev/null +++ b/app/Services/Streamer/Adapters/S3CompatibleStreamerAdapter.php @@ -0,0 +1,20 @@ +storage->getSongPresignedUrl($song)); + } +} diff --git a/app/Services/Streamer/Adapters/StreamerAdapter.php b/app/Services/Streamer/Adapters/StreamerAdapter.php new file mode 100644 index 00000000..e628ecc2 --- /dev/null +++ b/app/Services/Streamer/Adapters/StreamerAdapter.php @@ -0,0 +1,10 @@ +storage_metadata->getPath(); + + $bitRate = filter_var(Arr::get($config, 'bit_rate'), FILTER_SANITIZE_NUMBER_INT) + ?: config('koel.streaming.bitrate'); + + $startTime = filter_var(Arr::get($config, 'start_time', 0), FILTER_SANITIZE_NUMBER_FLOAT); + + setlocale(LC_CTYPE, 'en_US.UTF-8'); // #1481 special chars might be stripped otherwise + + header('Content-Type: audio/mpeg'); + header('Content-Disposition: attachment; filename="' . basename($path) . '"'); + + $args = [ + '-i ' . escapeshellarg($path), + '-map 0:0', + '-v 0', + "-ab {$bitRate}k", + '-f mp3', + '-', + ]; + + if ($startTime) { + array_unshift($args, "-ss $startTime"); + } + + passthru("$ffmpeg " . implode(' ', $args)); + } +} diff --git a/app/Services/Streamers/XAccelRedirectStreamer.php b/app/Services/Streamer/Adapters/XAccelRedirectStreamerAdapter.php similarity index 55% rename from app/Services/Streamers/XAccelRedirectStreamer.php rename to app/Services/Streamer/Adapters/XAccelRedirectStreamerAdapter.php index 8c046a78..c429e1cf 100644 --- a/app/Services/Streamers/XAccelRedirectStreamer.php +++ b/app/Services/Streamer/Adapters/XAccelRedirectStreamerAdapter.php @@ -1,25 +1,29 @@ song->path); + $path = $song->storage_metadata->getPath(); + $contentType = 'audio/' . pathinfo($path, PATHINFO_EXTENSION); + $relativePath = str_replace(Setting::get('media_path'), '', $path); // We send our media_path value as a 'X-Media-Root' header to downstream (nginx) // It will then be use as `alias` in X-Accel config location block. // See nginx.conf.example. header('X-Media-Root: ' . Setting::get('media_path')); header("X-Accel-Redirect: /media/$relativePath"); - header("Content-Type: $this->contentType"); - header('Content-Disposition: inline; filename="' . basename($this->song->path) . '"'); + header("Content-Type: $contentType"); + header('Content-Disposition: inline; filename="' . basename($path) . '"'); exit; } diff --git a/app/Services/Streamer/Adapters/XSendFileStreamerAdapter.php b/app/Services/Streamer/Adapters/XSendFileStreamerAdapter.php new file mode 100644 index 00000000..735aa3f9 --- /dev/null +++ b/app/Services/Streamer/Adapters/XSendFileStreamerAdapter.php @@ -0,0 +1,23 @@ +storage_metadata->getPath(); + $contentType = 'audio/' . pathinfo($path, PATHINFO_EXTENSION); + + header("X-Sendfile: $path"); + header("Content-Type: $contentType"); + header('Content-Disposition: inline; filename="' . basename($path) . '"'); + + exit; + } +} diff --git a/app/Services/Streamer/Streamer.php b/app/Services/Streamer/Streamer.php new file mode 100644 index 00000000..c67d95a6 --- /dev/null +++ b/app/Services/Streamer/Streamer.php @@ -0,0 +1,71 @@ +adapter = $adapter ?? $this->resolveAdapter(); + } + + private function resolveAdapter(): StreamerAdapter + { + throw_unless(SongStorageTypes::supported($this->song->storage), KoelPlusRequiredException::class); + + if ($this->shouldTranscode()) { + return app(TranscodingStreamerAdapter::class); + } + + return match ($this->song->storage) { + SongStorageTypes::LOCAL, '' => app(LocalStreamerAdapter::class), + SongStorageTypes::S3, SongStorageTypes::S3_LAMBDA => app(S3CompatibleStreamerAdapter::class), + SongStorageTypes::DROPBOX => app(DropboxStreamerAdapter::class), + default => throw UnsupportedSongStorageTypeException::make($this->song->storage), + }; + } + + public function stream(): mixed + { + return $this->adapter->stream($this->song, $this->config); + } + + private function shouldTranscode(): bool + { + // We only transcode local files. "Remote" transcoding (e.g., from Dropbox) is not supported. + if ($this->song->storage !== SongStorageTypes::LOCAL) { + return false; + } + + if (Arr::get($this->config, 'transcode', false)) { + return true; + } + + return $this->song->storage === SongStorageTypes::LOCAL + && Str::endsWith(File::mimeType($this->song->storage_metadata->getPath()), 'flac') + && config('koel.streaming.transcode_flac'); + } + + public function getAdapter(): StreamerAdapter + { + return $this->adapter; + } +} diff --git a/app/Services/Streamers/DropboxStreamer.php b/app/Services/Streamers/DropboxStreamer.php deleted file mode 100644 index 81636027..00000000 --- a/app/Services/Streamers/DropboxStreamer.php +++ /dev/null @@ -1,20 +0,0 @@ -storage->getSongPresignedUrl($this->song)); - } -} diff --git a/app/Services/Streamers/LocalStreamer.php b/app/Services/Streamers/LocalStreamer.php deleted file mode 100644 index e47c63e6..00000000 --- a/app/Services/Streamers/LocalStreamer.php +++ /dev/null @@ -1,22 +0,0 @@ -song = $song; - - // Hard code the content type instead of relying on PHP's fileinfo() - // or even Symfony's MIMETypeGuesser, since they appear to be wrong sometimes. - $this->contentType = 'audio/' . pathinfo($this->song->storage_metadata->getPath(), PATHINFO_EXTENSION); - } -} diff --git a/app/Services/Streamers/S3CompatibleStreamer.php b/app/Services/Streamers/S3CompatibleStreamer.php deleted file mode 100644 index 38cf10ef..00000000 --- a/app/Services/Streamers/S3CompatibleStreamer.php +++ /dev/null @@ -1,24 +0,0 @@ -storage->getSongPresignedUrl($this->song)); - } -} diff --git a/app/Services/Streamers/Streamer.php b/app/Services/Streamers/Streamer.php deleted file mode 100644 index bb6e831f..00000000 --- a/app/Services/Streamers/Streamer.php +++ /dev/null @@ -1,24 +0,0 @@ -song = $song; - } - - abstract public function stream(); // @phpcs:ignore -} diff --git a/app/Services/Streamers/StreamerFactory.php b/app/Services/Streamers/StreamerFactory.php deleted file mode 100644 index 5e83f66b..00000000 --- a/app/Services/Streamers/StreamerFactory.php +++ /dev/null @@ -1,50 +0,0 @@ -storage), KoelPlusRequiredException::class); - - if ($song->storage === SongStorageTypes::S3 || $song->storage === SongStorageTypes::S3_LAMBDA) { - return self::makeStreamerFromClass(S3CompatibleStreamer::class, $song); - } - - if ($song->storage === SongStorageTypes::DROPBOX) { - return self::makeStreamerFromClass(DropboxStreamer::class, $song); - } - - $transcode ??= $this->transcodingService->songShouldBeTranscoded($song); - - if ($transcode) { - /** @var TranscodingStreamer $streamer */ - $streamer = self::makeStreamerFromClass(TranscodingStreamer::class, $song); - $streamer->setBitRate($bitRate ?: config('koel.streaming.bitrate')); - $streamer->setStartTime($startTime); - - return $streamer; - } - - return self::makeStreamerFromClass(LocalStreamer::class, $song); - } - - private static function makeStreamerFromClass(string $class, Song $song): Streamer - { - return tap(app($class), static fn (Streamer $streamer) => $streamer->setSong($song)); - } -} diff --git a/app/Services/Streamers/TranscodingStreamer.php b/app/Services/Streamers/TranscodingStreamer.php deleted file mode 100644 index c632d2be..00000000 --- a/app/Services/Streamers/TranscodingStreamer.php +++ /dev/null @@ -1,62 +0,0 @@ -bitRate, FILTER_SANITIZE_NUMBER_INT); - - header('Content-Type: audio/mpeg'); - header('Content-Disposition: attachment; filename="' . basename($this->song->path) . '"'); - - $args = [ - '-i ' . escapeshellarg($this->song->path), - '-map 0:0', - '-v 0', - "-ab {$bitRate}k", - '-f mp3', - '-', - ]; - - if ($this->startTime) { - array_unshift($args, "-ss $this->startTime"); - } - - passthru("$ffmpeg " . implode(' ', $args)); - } - - public function setBitRate(int $bitRate): void - { - $this->bitRate = $bitRate; - } - - public function setStartTime(float $startTime): void - { - $this->startTime = $startTime; - } - - protected function supported(): bool - { - return true; - } -} diff --git a/app/Services/Streamers/XSendFileStreamer.php b/app/Services/Streamers/XSendFileStreamer.php deleted file mode 100644 index 40a677e0..00000000 --- a/app/Services/Streamers/XSendFileStreamer.php +++ /dev/null @@ -1,18 +0,0 @@ -song->path}"); - header("Content-Type: $this->contentType"); - header('Content-Disposition: inline; filename="' . basename($this->song->path) . '"'); - - exit; - } -} diff --git a/app/Services/TranscodingService.php b/app/Services/TranscodingService.php deleted file mode 100644 index 8fff5e06..00000000 --- a/app/Services/TranscodingService.php +++ /dev/null @@ -1,13 +0,0 @@ -path), 'flac') && config('koel.streaming.transcode_flac'); - } -} diff --git a/storage/framework/testing/disks/s3/1__01hqcp4z8be6jf58jzdnsr2y0j__song.mp3 b/storage/framework/testing/disks/s3/1__01hqd2pnm2y0wvsyx5cerwh0k1__song.mp3 similarity index 100% rename from storage/framework/testing/disks/s3/1__01hqcp4z8be6jf58jzdnsr2y0j__song.mp3 rename to storage/framework/testing/disks/s3/1__01hqd2pnm2y0wvsyx5cerwh0k1__song.mp3 diff --git a/tests/Feature/KoelPlus/SongPlayTest.php b/tests/Feature/KoelPlus/SongPlayTest.php index 4f7a95ed..797481d4 100644 --- a/tests/Feature/KoelPlus/SongPlayTest.php +++ b/tests/Feature/KoelPlus/SongPlayTest.php @@ -3,10 +3,9 @@ namespace Tests\Feature\KoelPlus; use App\Models\Song; -use App\Services\Streamers\LocalStreamer; +use App\Services\Streamer\Adapters\LocalStreamerAdapter; use App\Services\TokenManager; use App\Values\CompositeToken; -use Mockery; use Tests\PlusTestCase; use function Tests\create_user; @@ -24,13 +23,9 @@ class SongPlayTest extends PlusTestCase 'path' => test_path('songs/blank.mp3'), ]); - $mockStreamer = $this->mock(LocalStreamer::class); - - $mockStreamer->shouldReceive('setSong')->with( - Mockery::on(static fn (Song $retrievedSong): bool => $retrievedSong->id === $song->id) - )->once(); - - $mockStreamer->shouldReceive('stream')->once(); + $this->mock(LocalStreamerAdapter::class) + ->shouldReceive('stream') + ->once(); $this->get("play/$song->id?t=$token->audioToken") ->assertOk(); @@ -46,13 +41,9 @@ class SongPlayTest extends PlusTestCase /** @var CompositeToken $token */ $token = app(TokenManager::class)->createCompositeToken($song->owner); - $mockStreamer = $this->mock(LocalStreamer::class); - - $mockStreamer->shouldReceive('setSong')->with( - Mockery::on(static fn (Song $retrievedSong): bool => $retrievedSong->id === $song->id) - )->once(); - - $mockStreamer->shouldReceive('stream')->once(); + $this->mock(LocalStreamerAdapter::class) + ->shouldReceive('stream') + ->once(); $this->get("play/$song->id?t=$token->audioToken") ->assertOk(); diff --git a/tests/Feature/SongPlayTest.php b/tests/Feature/SongPlayTest.php index 58d5df75..d0f5f56b 100644 --- a/tests/Feature/SongPlayTest.php +++ b/tests/Feature/SongPlayTest.php @@ -3,10 +3,11 @@ namespace Tests\Feature; use App\Models\Song; -use App\Services\Streamers\LocalStreamer; +use App\Services\Streamer\Adapters\LocalStreamerAdapter; +use App\Services\Streamer\Adapters\TranscodingStreamerAdapter; use App\Services\TokenManager; use App\Values\CompositeToken; -use Mockery; +use Illuminate\Support\Facades\File; use Tests\TestCase; use function Tests\create_user; @@ -26,15 +27,55 @@ class SongPlayTest extends TestCase 'path' => test_path('songs/blank.mp3'), ]); - $mockStreamer = $this->mock(LocalStreamer::class); - - $mockStreamer->shouldReceive('setSong')->with( - Mockery::on(static fn (Song $retrievedSong): bool => $retrievedSong->id === $song->id) - )->once(); - - $mockStreamer->shouldReceive('stream')->once(); + $this->mock(LocalStreamerAdapter::class) + ->shouldReceive('stream') + ->once(); $this->get("play/$song->id?t=$token->audioToken") ->assertOk(); } + + public function testTranscoding(): void + { + config(['koel.streaming.transcode_flac' => true]); + $user = create_user(); + + /** @var CompositeToken $token */ + $token = app(TokenManager::class)->createCompositeToken($user); + + /** @var Song $song */ + $song = Song::factory()->create(['path' => test_path('songs/blank.mp3')]); + + File::partialMock() + ->shouldReceive('mimeType') + ->with($song->path) + ->andReturn('audio/flac'); + + $this->mock(TranscodingStreamerAdapter::class) + ->shouldReceive('stream') + ->once(); + + $this->get("play/$song->id?t=$token->audioToken") + ->assertOk(); + + config(['koel.streaming.transcode_flac' => false]); + } + + public function testForceTranscoding(): void + { + $user = create_user(); + + /** @var CompositeToken $token */ + $token = app(TokenManager::class)->createCompositeToken($user); + + /** @var Song $song */ + $song = Song::factory()->create(['path' => test_path('songs/blank.mp3')]); + + $this->mock(TranscodingStreamerAdapter::class) + ->shouldReceive('stream') + ->once(); + + $this->get("play/$song->id/1/128?t=$token->audioToken") + ->assertOk(); + } } diff --git a/tests/Integration/Factories/StreamerFactoryTest.php b/tests/Integration/Factories/StreamerFactoryTest.php deleted file mode 100644 index 6f6f6b44..00000000 --- a/tests/Integration/Factories/StreamerFactoryTest.php +++ /dev/null @@ -1,130 +0,0 @@ -streamerFactory = app(StreamerFactory::class); - PHPMockery::mock('App\Services\Streamers', 'file_exists')->andReturn(true); - } - - public function testCreateStreamer(): void - { - collect(SongStorageTypes::ALL_TYPES) - ->each(function (?string $type): void { - switch ($type) { - case SongStorageTypes::S3: - self::expectException(KoelPlusRequiredException::class); - $this->streamerFactory->createStreamer( - Song::factory()->make(['path' => "s3://bucket/foo.mp3", 'storage' => $type]) - ); - break; - - case SongStorageTypes::S3_LAMBDA: - self::assertInstanceOf(S3CompatibleStreamer::class, $this->streamerFactory->createStreamer( - Song::factory()->make(['path' => "s3://bucket/foo.mp3", 'storage' => $type]) - )); - break; - - case SongStorageTypes::DROPBOX: - self::expectException(KoelPlusRequiredException::class); - $this->streamerFactory->createStreamer( - Song::factory()->make(['path' => "dropbox://foo.mp3", 'storage' => $type]) - ); - break; - - case SongStorageTypes::LOCAL: - self::assertInstanceOf(PhpStreamer::class, $this->streamerFactory->createStreamer( - Song::factory()->make(['path' => test_path('songs/blank.mp3'), 'storage' => $type]) - )); - break; - - default: - throw new Exception("Unhandled storage type: $type"); - } - }); - } - - public function testCreateTranscodingStreamerIfSupported(): void - { - $this->swap(TranscodingService::class, Mockery::mock(TranscodingService::class, [ - 'songShouldBeTranscoded' => true, - ])); - - /** @var StreamerFactory $streamerFactory */ - $streamerFactory = app(StreamerFactory::class); - - /** @var Song $song */ - $song = Song::factory()->make(['path' => test_path('songs/blank.mp3')]); - self::assertInstanceOf(TranscodingStreamer::class, $streamerFactory->createStreamer($song)); - } - - public function testCreateTranscodingStreamerIfForced(): void - { - $this->swap(TranscodingService::class, Mockery::mock(TranscodingService::class, [ - 'songShouldBeTranscoded' => false, - ])); - - /** @var StreamerFactory $streamerFactory */ - $streamerFactory = app(StreamerFactory::class); - - /** @var Song $song */ - $song = Song::factory()->make(['path' => test_path('songs/blank.mp3')]); - self::assertInstanceOf(TranscodingStreamer::class, $streamerFactory->createStreamer($song, true)); - } - - /** @return array */ - public function provideStreamingConfigData(): array - { - return [ - [null, PhpStreamer::class], - ['x-sendfile', XSendFileStreamer::class], - ['x-accel-redirect', XAccelRedirectStreamer::class], - ]; - } - - /** - * @dataProvider provideStreamingConfigData - * - * @param string|null $config - * @param string $expectedClass - */ - public function testCreatePhpStreamer($config, $expectedClass): void - { - $this->swap(TranscodingService::class, Mockery::mock(TranscodingService::class, [ - 'songShouldBeTranscoded' => false, - ])); - - config(['koel.streaming.method' => $config]); - - /** @var StreamerFactory $streamerFactory */ - $streamerFactory = app(StreamerFactory::class); - - /** @var Song $song */ - $song = Song::factory()->make(['path' => test_path('songs/blank.mp3')]); - self::assertInstanceOf($expectedClass, $streamerFactory->createStreamer($song)); - } -} diff --git a/tests/Integration/KoelPlus/Services/Streamer/StreamerTest.php b/tests/Integration/KoelPlus/Services/Streamer/StreamerTest.php new file mode 100644 index 00000000..0d3514bd --- /dev/null +++ b/tests/Integration/KoelPlus/Services/Streamer/StreamerTest.php @@ -0,0 +1,47 @@ +shouldReceive('mimeType')->andReturn('audio/mpeg'); + + collect(SongStorageTypes::ALL_TYPES) + ->each(static function (?string $type): void { + /** @var Song $song */ + $song = Song::factory()->make(['storage' => $type]); + $streamer = new Streamer($song); + + switch ($type) { + case SongStorageTypes::S3: + case SongStorageTypes::S3_LAMBDA: + self::assertInstanceOf(S3CompatibleStreamerAdapter::class, $streamer->getAdapter()); + break; + + case SongStorageTypes::DROPBOX: + self::assertInstanceOf(DropboxStreamerAdapter::class, $streamer->getAdapter()); + break; + + case SongStorageTypes::LOCAL: + case '': + self::assertInstanceOf(LocalStreamerAdapter::class, $streamer->getAdapter()); + break; + + default: + throw new Exception("Unhandled storage type: $type"); + } + }); + } +} diff --git a/tests/Integration/KoelPlus/StreamerFactoryTest.php b/tests/Integration/KoelPlus/StreamerFactoryTest.php deleted file mode 100644 index 2b09d3b9..00000000 --- a/tests/Integration/KoelPlus/StreamerFactoryTest.php +++ /dev/null @@ -1,57 +0,0 @@ -streamerFactory = app(StreamerFactory::class); - PHPMockery::mock('App\Services\Streamers', 'file_exists')->andReturn(true); - } - - public function testCreateStreamer(): void - { - collect(SongStorageTypes::ALL_TYPES) - ->each(function (?string $type): void { - switch ($type) { - case SongStorageTypes::S3: - case SongStorageTypes::S3_LAMBDA: - self::assertInstanceOf(S3CompatibleStreamer::class, $this->streamerFactory->createStreamer( - Song::factory()->make(['path' => "s3://bucket/foo.mp3", 'storage' => $type]) - )); - break; - - case SongStorageTypes::DROPBOX: - $this->streamerFactory->createStreamer( - Song::factory()->make(['path' => "dropbox://foo.mp3", 'storage' => $type]) - ); - break; - - case SongStorageTypes::LOCAL: - self::assertInstanceOf(PhpStreamer::class, $this->streamerFactory->createStreamer( - Song::factory()->make(['path' => test_path('songs/blank.mp3'), 'storage' => $type]) - )); - break; - - default: - throw new Exception("Unhandled storage type: $type"); - } - }); - } -} diff --git a/tests/Integration/Services/Streamer/StreamerTest.php b/tests/Integration/Services/Streamer/StreamerTest.php new file mode 100644 index 00000000..e76654d9 --- /dev/null +++ b/tests/Integration/Services/Streamer/StreamerTest.php @@ -0,0 +1,97 @@ +each(static function (?string $type): void { + /** @var Song $song */ + $song = Song::factory()->make(['storage' => $type]); + + switch ($type) { + case SongStorageTypes::S3: + case SongStorageTypes::DROPBOX: + self::expectException(KoelPlusRequiredException::class); + new Streamer($song); + break; + + case SongStorageTypes::S3_LAMBDA: + self::assertInstanceOf(S3CompatibleStreamerAdapter::class, (new Streamer($song))->getAdapter()); + break; + + case SongStorageTypes::LOCAL: + self::assertInstanceOf(LocalStreamerAdapter::class, (new Streamer($song))->getAdapter()); + break; + + default: + throw new Exception("Unhandled storage type: $type"); + } + }); + } + + public function testResolveTranscodingAdapter(): void + { + config(['koel.streaming.transcode_flac' => true]); + + File::partialMock()->shouldReceive('mimeType')->andReturn('audio/flac'); + + /** @var Song $song */ + $song = Song::factory()->make(['path' => test_path('songs/blank.mp3')]); + self::assertInstanceOf(TranscodingStreamerAdapter::class, (new Streamer($song))->getAdapter()); + + config(['koel.streaming.transcode_flac' => false]); + } + + public function testForceTranscodingAdapter(): void + { + /** @var Song $song */ + $song = Song::factory()->make(['path' => test_path('songs/blank.mp3')]); + + self::assertInstanceOf( + TranscodingStreamerAdapter::class, + (new Streamer($song, null, ['transcode' => true]))->getAdapter() + ); + } + + /** @return array */ + public function provideStreamConfigData(): array + { + return [ + PhpStreamerAdapter::class => [null, PhpStreamerAdapter::class], + XSendFileStreamerAdapter::class => ['x-sendfile', XSendFileStreamerAdapter::class], + XAccelRedirectStreamerAdapter::class => ['x-accel-redirect', XAccelRedirectStreamerAdapter::class], + ]; + } + + /** @dataProvider provideStreamConfigData */ + public function testResolveLocalAdapter(?string $config, string $expectedClass): void + { + config(['koel.streaming.method' => $config]); + + /** @var Song $song */ + $song = Song::factory()->make(['path' => test_path('songs/blank.mp3')]); + + self::assertInstanceOf($expectedClass, (new Streamer($song))->getAdapter()); + + config(['koel.streaming.method' => null]); + } +}