mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: get album thumbnail from the server
This commit is contained in:
parent
40d4671b28
commit
6977cc4986
10 changed files with 206 additions and 53 deletions
34
app/Http/Controllers/API/AlbumThumbnailController.php
Normal file
34
app/Http/Controllers/API/AlbumThumbnailController.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @group 5. Media information
|
||||
*/
|
||||
class AlbumThumbnailController extends Controller
|
||||
{
|
||||
private $mediaMetadataService;
|
||||
|
||||
public function __construct(MediaMetadataService $mediaMetadataService)
|
||||
{
|
||||
$this->mediaMetadataService = $mediaMetadataService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an album's thumbnail
|
||||
*
|
||||
* Get an album's thumbnail (a 48px-wide version of the album's cover). Returns the full URL to the thumbnail,
|
||||
* or NULL if the album has no cover.
|
||||
*
|
||||
* @response ["thumbnailUrl", "https://localhost/public/img/covers/a146d01afb742b01f28ab8b556f9a75d_thumbnail.jpg"]
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function get(Album $album): JsonResponse
|
||||
{
|
||||
return response()->json(['thumbnailUrl' => $this->mediaMetadataService->getAlbumThumbnailUrl($album)]);
|
||||
}
|
||||
}
|
|
@ -100,7 +100,7 @@ class Album extends Model
|
|||
return null;
|
||||
}
|
||||
|
||||
return public_path("/public/img/covers/{$this->cover}");
|
||||
return public_path("/public/img/covers/$cover");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,8 +7,8 @@ use Intervention\Image\ImageManager;
|
|||
|
||||
class ImageWriter
|
||||
{
|
||||
private const MAX_WIDTH = 500;
|
||||
private const QUALITY = 80;
|
||||
private const DEFAULT_MAX_WIDTH = 500;
|
||||
private const DEFAULT_QUALITY = 80;
|
||||
|
||||
private $imageManager;
|
||||
|
||||
|
@ -17,14 +17,17 @@ class ImageWriter
|
|||
$this->imageManager = $imageManager;
|
||||
}
|
||||
|
||||
public function writeFromBinaryData(string $destination, string $data): void
|
||||
public function writeFromBinaryData(string $destination, string $data, array $config = []): void
|
||||
{
|
||||
$this->imageManager
|
||||
->make($data)
|
||||
->resize(self::MAX_WIDTH, null, static function (Constraint $constraint): void {
|
||||
$constraint->upsize();
|
||||
$constraint->aspectRatio();
|
||||
})
|
||||
->save($destination, self::QUALITY);
|
||||
->resize(
|
||||
$config['max_width'] ?? self::DEFAULT_MAX_WIDTH,
|
||||
null, static function (Constraint $constraint): void {
|
||||
$constraint->upsize();
|
||||
$constraint->aspectRatio();
|
||||
}
|
||||
)
|
||||
->save($destination, $config['quality'] ?? self::DEFAULT_QUALITY);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,4 +95,25 @@ class MediaMetadataService
|
|||
{
|
||||
return sprintf('%s/public/img/artists/%s.%s', app()->publicPath(), sha1(uniqid()), $extension);
|
||||
}
|
||||
|
||||
public function getAlbumThumbnailUrl(Album $album): ?string
|
||||
{
|
||||
if (!$album->has_cover) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = pathinfo($album->cover_path);
|
||||
$thumbnail = sprintf('%s_thumb.%s', $parts['filename'], $parts['extension']);
|
||||
$thumbnailPath = public_path("/public/img/covers/$thumbnail");
|
||||
|
||||
if (!file_exists($thumbnailPath)) {
|
||||
$this->imageWriter->writeFromBinaryData(
|
||||
$thumbnailPath,
|
||||
file_get_contents($album->cover_path),
|
||||
['max_width' => 48]
|
||||
);
|
||||
}
|
||||
|
||||
return app()->staticUrl("public/img/covers/$thumbnail");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 468b538a538b9b85df895efaabbf25eb305d13ac
|
||||
Subproject commit 428ab7414fa77533b1f1d6fadee23999e13b5a4b
|
|
@ -88,6 +88,8 @@ Route::group(['namespace' => 'API'], function () {
|
|||
Route::put('album/{album}/cover', 'AlbumCoverController@update');
|
||||
Route::put('artist/{artist}/image', 'ArtistImageController@update');
|
||||
|
||||
Route::get('album/{album}/thumbnail', 'AlbumThumbnailController@get');
|
||||
|
||||
// iTunes routes
|
||||
if (iTunes::used()) {
|
||||
Route::get('itunes/song/{album}', 'iTunesController@viewSong')->name('iTunes.viewSong');
|
||||
|
|
|
@ -22,24 +22,20 @@ trait CreatesApplication
|
|||
*/
|
||||
protected $baseUrl = 'http://localhost';
|
||||
|
||||
/**
|
||||
* Creates the application.
|
||||
*
|
||||
* @return Application
|
||||
*/
|
||||
public function createApplication()
|
||||
public function createApplication(): Application
|
||||
{
|
||||
/** @var Application $app */
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$this->artisan = $app->make(Artisan::class);
|
||||
$this->artisan->bootstrap();
|
||||
|
||||
$this->coverPath = $app->basePath().'/public/img/covers';
|
||||
$this->coverPath = $app->basePath('/public/img/covers');
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
private function prepareForTests()
|
||||
private function prepareForTests(): void
|
||||
{
|
||||
$this->artisan->call('migrate');
|
||||
|
||||
|
|
47
tests/Feature/AlbumThumbnailTest.php
Normal file
47
tests/Feature/AlbumThumbnailTest.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
class AlbumThumbnailTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var MediaMetadataService|MockInterface
|
||||
*/
|
||||
private $mediaMetadataService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->mediaMetadataService = self::mockIocDependency(MediaMetadataService::class);
|
||||
}
|
||||
|
||||
public function provideAlbumThumbnailData(): array
|
||||
{
|
||||
return [['http://localhost/public/img/covers/foo_thumbnail.jpg'], [null]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAlbumThumbnailData
|
||||
*/
|
||||
public function testGetAlbumThumbnail(?string $thumbnailUrl): void
|
||||
{
|
||||
/** @var Album $createdAlbum */
|
||||
$createdAlbum = factory(Album::class)->create();
|
||||
|
||||
$this->mediaMetadataService
|
||||
->shouldReceive('getAlbumThumbnailUrl')
|
||||
->once()
|
||||
->with(Mockery::on(static function (Album $album) use ($createdAlbum): bool {
|
||||
return $album->id === $createdAlbum->id;
|
||||
}))
|
||||
->andReturn($thumbnailUrl);
|
||||
|
||||
$this->getAsUser("api/album/{$createdAlbum->id}/thumbnail")
|
||||
->seeJson(['thumbnailUrl' => $thumbnailUrl]);
|
||||
}
|
||||
}
|
|
@ -3,58 +3,48 @@
|
|||
namespace Tests\Integration\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Services\ImageWriter;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Illuminate\Log\Logger;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MediaMetadataServiceTest extends TestCase
|
||||
{
|
||||
/** @var MediaMetadataService */
|
||||
private $mediaMetadataService;
|
||||
|
||||
/** @var ImageWriter|MockInterface */
|
||||
private $imageWriter;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->imageWriter = Mockery::mock(ImageWriter::class);
|
||||
$this->mediaMetadataService = new MediaMetadataService($this->imageWriter, app(Logger::class));
|
||||
$this->cleanUp();
|
||||
}
|
||||
|
||||
public function testWriteAlbumCover(): void
|
||||
public function testGetAlbumThumbnailUrl(): void
|
||||
{
|
||||
/** @var Album $album */
|
||||
$album = factory(Album::class)->create();
|
||||
$coverContent = 'dummy';
|
||||
$coverPath = '/koel/public/images/album/foo.jpg';
|
||||
copy(__DIR__ . '/../../blobs/cover.png', $this->coverPath . '/album-cover-for-thumbnail-test.jpg');
|
||||
|
||||
$this->imageWriter
|
||||
->shouldReceive('writeFromBinaryData')
|
||||
->once()
|
||||
->with('/koel/public/images/album/foo.jpg', 'dummy');
|
||||
$album = factory(Album::class)->create(['cover' => 'album-cover-for-thumbnail-test.jpg']);
|
||||
|
||||
$this->mediaMetadataService->writeAlbumCover($album, $coverContent, 'jpg', $coverPath);
|
||||
$this->assertEquals('http://localhost/public/img/covers/foo.jpg', Album::find($album->id)->cover);
|
||||
self::assertSame(
|
||||
app()->staticUrl('public/img/covers/album-cover-for-thumbnail-test_thumb.jpg'),
|
||||
app()->get(MediaMetadataService::class)->getAlbumThumbnailUrl($album)
|
||||
);
|
||||
|
||||
self::assertFileExists($this->coverPath . '/album-cover-for-thumbnail-test_thumb.jpg');
|
||||
}
|
||||
|
||||
public function testWriteArtistImage(): void
|
||||
public function testGetAlbumThumbnailUrlWithNoCover(): void
|
||||
{
|
||||
$artist = factory(Artist::class)->create();
|
||||
$imageContent = 'dummy';
|
||||
$imagePath = '/koel/public/images/artist/foo.jpg';
|
||||
$album = factory(Album::class)->create(['cover' => null]);
|
||||
self::assertNull(app()->get(MediaMetadataService::class)->getAlbumThumbnailUrl($album));
|
||||
}
|
||||
|
||||
$this->imageWriter
|
||||
->shouldReceive('writeFromBinaryData')
|
||||
->once()
|
||||
->with('/koel/public/images/artist/foo.jpg', 'dummy');
|
||||
private function cleanUp(): void
|
||||
{
|
||||
@unlink($this->coverPath . '/album-cover-for-thumbnail-test.jpg');
|
||||
@unlink($this->coverPath . '/album-cover-for-thumbnail-test_thumb.jpg');
|
||||
self::assertFileNotExists($this->coverPath . '/album-cover-for-thumbnail-test.jpg');
|
||||
self::assertFileNotExists($this->coverPath . '/album-cover-for-thumbnail-test_thumb.jpg');
|
||||
}
|
||||
|
||||
$this->mediaMetadataService->writeArtistImage($artist, $imageContent, 'jpg', $imagePath);
|
||||
$this->assertEquals('http://localhost/public/img/artists/foo.jpg', Artist::find($artist->id)->image);
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->cleanUp();
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
||||
|
|
60
tests/Unit/Services/MediaMetadataServiceTest.php
Normal file
60
tests/Unit/Services/MediaMetadataServiceTest.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Services\ImageWriter;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Illuminate\Log\Logger;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MediaMetadataServiceTest extends TestCase
|
||||
{
|
||||
/** @var MediaMetadataService */
|
||||
private $mediaMetadataService;
|
||||
|
||||
/** @var ImageWriter|MockInterface */
|
||||
private $imageWriter;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->imageWriter = Mockery::mock(ImageWriter::class);
|
||||
$this->mediaMetadataService = new MediaMetadataService($this->imageWriter, app(Logger::class));
|
||||
}
|
||||
|
||||
public function testWriteAlbumCover(): void
|
||||
{
|
||||
/** @var Album $album */
|
||||
$album = factory(Album::class)->create();
|
||||
$coverContent = 'dummy';
|
||||
$coverPath = '/koel/public/images/album/foo.jpg';
|
||||
|
||||
$this->imageWriter
|
||||
->shouldReceive('writeFromBinaryData')
|
||||
->once()
|
||||
->with('/koel/public/images/album/foo.jpg', 'dummy');
|
||||
|
||||
$this->mediaMetadataService->writeAlbumCover($album, $coverContent, 'jpg', $coverPath);
|
||||
$this->assertEquals('http://localhost/public/img/covers/foo.jpg', Album::find($album->id)->cover);
|
||||
}
|
||||
|
||||
public function testWriteArtistImage(): void
|
||||
{
|
||||
$artist = factory(Artist::class)->create();
|
||||
$imageContent = 'dummy';
|
||||
$imagePath = '/koel/public/images/artist/foo.jpg';
|
||||
|
||||
$this->imageWriter
|
||||
->shouldReceive('writeFromBinaryData')
|
||||
->once()
|
||||
->with('/koel/public/images/artist/foo.jpg', 'dummy');
|
||||
|
||||
$this->mediaMetadataService->writeArtistImage($artist, $imageContent, 'jpg', $imagePath);
|
||||
$this->assertEquals('http://localhost/public/img/artists/foo.jpg', Artist::find($artist->id)->image);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue