feat: get album thumbnail from the server

This commit is contained in:
Phan An 2020-06-12 15:55:45 +02:00
parent 40d4671b28
commit 6977cc4986
10 changed files with 206 additions and 53 deletions

View 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)]);
}
}

View file

@ -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");
}
/**

View file

@ -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);
}
}

View file

@ -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

View file

@ -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');

View file

@ -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');

View 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]);
}
}

View file

@ -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();
}
}

View 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);
}
}