mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: integrate with Spotify
This commit is contained in:
parent
1e38150f26
commit
878815659f
29 changed files with 262 additions and 258 deletions
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Values\AlbumInformation;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class AlbumInformationFetched extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(public Album $album, public AlbumInformation $information)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Artist;
|
||||
use App\Values\ArtistInformation;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ArtistInformationFetched
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(public Artist $artist, public ArtistInformation $information)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ use App\Repositories\SongRepository;
|
|||
use App\Services\ApplicationInformationService;
|
||||
use App\Services\ITunesService;
|
||||
use App\Services\LastfmService;
|
||||
use App\Services\SpotifyService;
|
||||
use App\Services\YouTubeService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
|
@ -36,7 +37,8 @@ class DataController extends Controller
|
|||
'playlists' => $this->playlistRepository->getAllByCurrentUser(),
|
||||
'current_user' => UserResource::make($this->user),
|
||||
'use_last_fm' => $this->lastfmService->used(),
|
||||
'use_you_tube' => $this->youTubeService->enabled(),
|
||||
'use_spotify' => SpotifyService::enabled(),
|
||||
'use_you_tube' => $this->youTubeService->enabled(), // @todo clean this mess up
|
||||
'use_i_tunes' => $this->iTunesService->used(),
|
||||
'allow_download' => config('koel.download.allow'),
|
||||
'supports_transcoding' => config('koel.streaming.ffmpeg_path')
|
||||
|
|
11
app/Http/Requests/SpotifyCallbackRequest.php
Normal file
11
app/Http/Requests/SpotifyCallbackRequest.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
/**
|
||||
* @property-read string $state
|
||||
* @property-read string $code
|
||||
*/
|
||||
class SpotifyCallbackRequest extends AbstractRequest
|
||||
{
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\AlbumInformationFetched;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Throwable;
|
||||
|
||||
class DownloadAlbumCover
|
||||
{
|
||||
public function __construct(private MediaMetadataService $mediaMetadataService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(AlbumInformationFetched $event): void
|
||||
{
|
||||
// If our current album has no cover, and Last.fm has one, steal it?
|
||||
if (!$event->album->has_cover && $event->information->cover && ini_get('allow_url_fopen')) {
|
||||
try {
|
||||
$this->mediaMetadataService->downloadAlbumCover($event->album, $event->information->cover);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\ArtistInformationFetched;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Throwable;
|
||||
|
||||
class DownloadArtistImage
|
||||
{
|
||||
public function __construct(private MediaMetadataService $mediaMetadataService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(ArtistInformationFetched $event): void
|
||||
{
|
||||
// If our artist has no image, and Last.fm has one, we steal it?
|
||||
if (!$event->artist->has_image && $event->information->image && ini_get('allow_url_fopen')) {
|
||||
try {
|
||||
$this->mediaMetadataService->downloadArtistImage($event->artist, $event->information->image);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Events\AlbumInformationFetched;
|
||||
use App\Events\ArtistInformationFetched;
|
||||
use App\Events\LibraryChanged;
|
||||
use App\Events\MediaSyncCompleted;
|
||||
use App\Events\SongLikeToggled;
|
||||
|
@ -12,8 +10,6 @@ use App\Events\SongsBatchUnliked;
|
|||
use App\Events\SongStartedPlaying;
|
||||
use App\Listeners\ClearMediaCache;
|
||||
use App\Listeners\DeleteNonExistingRecordsPostSync;
|
||||
use App\Listeners\DownloadAlbumCover;
|
||||
use App\Listeners\DownloadArtistImage;
|
||||
use App\Listeners\LoveMultipleTracksOnLastfm;
|
||||
use App\Listeners\LoveTrackOnLastfm;
|
||||
use App\Listeners\PruneLibrary;
|
||||
|
@ -49,14 +45,6 @@ class EventServiceProvider extends ServiceProvider
|
|||
ClearMediaCache::class,
|
||||
],
|
||||
|
||||
AlbumInformationFetched::class => [
|
||||
DownloadAlbumCover::class,
|
||||
],
|
||||
|
||||
ArtistInformationFetched::class => [
|
||||
DownloadArtistImage::class,
|
||||
],
|
||||
|
||||
MediaSyncCompleted::class => [
|
||||
DeleteNonExistingRecordsPostSync::class,
|
||||
],
|
||||
|
|
|
@ -13,7 +13,7 @@ use SimpleXMLElement;
|
|||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @method object get(string $uri, array $data = [], bool $appendKey = true)
|
||||
* @method object|null get(string $uri, array $data = [], bool $appendKey = true)
|
||||
* @method object post($uri, array $data = [], bool $appendKey = true)
|
||||
* @method object put($uri, array $data = [], bool $appendKey = true)
|
||||
* @method object patch($uri, array $data = [], bool $appendKey = true)
|
||||
|
|
|
@ -128,7 +128,7 @@ class FileSynchronizer
|
|||
|
||||
if ($cover) {
|
||||
$extension = pathinfo($cover, PATHINFO_EXTENSION);
|
||||
$this->mediaMetadataService->writeAlbumCover($album, file_get_contents($cover), $extension);
|
||||
$this->mediaMetadataService->writeAlbumCover($album, $cover, $extension);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ class ImageWriter
|
|||
$this->imageManager = $imageManager;
|
||||
}
|
||||
|
||||
public function writeFromBinaryData(string $destination, string $data, array $config = []): void
|
||||
public function write(string $destination, object|string $source, array $config = []): void
|
||||
{
|
||||
$img = $this->imageManager
|
||||
->make($data)
|
||||
->make($source)
|
||||
->resize(
|
||||
$config['max_width'] ?? self::DEFAULT_MAX_WIDTH,
|
||||
null,
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\User;
|
||||
use App\Values\AlbumInformation;
|
||||
use App\Values\ArtistInformation;
|
||||
|
@ -34,13 +36,13 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
|
|||
return $this->getKey() && $this->getSecret();
|
||||
}
|
||||
|
||||
public function getArtistInformation(string $name): ?ArtistInformation
|
||||
public function getArtistInformation(Artist $artist): ?ArtistInformation
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = urlencode($name);
|
||||
$name = urlencode($artist->name);
|
||||
|
||||
try {
|
||||
return $this->cache->remember(
|
||||
|
@ -59,14 +61,14 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
|
|||
}
|
||||
}
|
||||
|
||||
public function getAlbumInformation(string $albumName, string $artistName): ?AlbumInformation
|
||||
public function getAlbumInformation(Album $album): ?AlbumInformation
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$albumName = urlencode($albumName);
|
||||
$artistName = urlencode($artistName);
|
||||
$albumName = urlencode($album->name);
|
||||
$artistName = urlencode($album->artist->name);
|
||||
|
||||
try {
|
||||
$cacheKey = md5("lastfm_album_{$albumName}_{$artistName}");
|
||||
|
|
|
@ -2,21 +2,18 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Events\AlbumInformationFetched;
|
||||
use App\Events\ArtistInformationFetched;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Repositories\AlbumRepository;
|
||||
use App\Repositories\ArtistRepository;
|
||||
use App\Values\AlbumInformation;
|
||||
use App\Values\ArtistInformation;
|
||||
use Throwable;
|
||||
|
||||
class MediaInformationService
|
||||
{
|
||||
public function __construct(
|
||||
private LastfmService $lastfmService,
|
||||
private AlbumRepository $albumRepository,
|
||||
private ArtistRepository $artistRepository
|
||||
private SpotifyService $spotifyService,
|
||||
private MediaMetadataService $mediaMetadataService
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -26,12 +23,18 @@ class MediaInformationService
|
|||
return null;
|
||||
}
|
||||
|
||||
$info = $this->lastfmService->getAlbumInformation($album->name, $album->artist->name);
|
||||
$info = $this->lastfmService->getAlbumInformation($album) ?: new AlbumInformation();
|
||||
|
||||
if ($info) {
|
||||
event(new AlbumInformationFetched($album, $info));
|
||||
// The album cover may have been updated.
|
||||
$info->cover = $this->albumRepository->getOneById($album->id)->cover;
|
||||
if (!$album->has_cover) {
|
||||
try {
|
||||
$cover = $this->spotifyService->tryGetAlbumCover($album);
|
||||
|
||||
if ($cover) {
|
||||
$this->mediaMetadataService->downloadAlbumCover($album, $cover);
|
||||
$info->cover = $album->refresh()->cover;
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
return $info;
|
||||
|
@ -43,12 +46,18 @@ class MediaInformationService
|
|||
return null;
|
||||
}
|
||||
|
||||
$info = $this->lastfmService->getArtistInformation($artist->name);
|
||||
$info = $this->lastfmService->getArtistInformation($artist) ?: new ArtistInformation();
|
||||
|
||||
if ($info) {
|
||||
event(new ArtistInformationFetched($artist, $info));
|
||||
// The artist image may have been updated.
|
||||
$info->image = $this->artistRepository->getOneById($artist->id)->image;
|
||||
if (!$artist->has_image) {
|
||||
try {
|
||||
$image = $this->spotifyService->tryGetArtistImage($artist);
|
||||
|
||||
if ($image) {
|
||||
$this->mediaMetadataService->downloadArtistImage($artist, $image);
|
||||
$info->image = $artist->refresh()->image;
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
return $info;
|
||||
|
|
|
@ -20,26 +20,26 @@ class MediaMetadataService
|
|||
|
||||
public function downloadAlbumCover(Album $album, string $imageUrl): void
|
||||
{
|
||||
$extension = explode('.', $imageUrl);
|
||||
$this->writeAlbumCover($album, file_get_contents($imageUrl), last($extension));
|
||||
$this->writeAlbumCover($album, $imageUrl, 'png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an album cover image file with binary data and update the Album with the new cover attribute.
|
||||
*
|
||||
* @param string $source Path, URL, or even binary data. See https://image.intervention.io/v2/api/make.
|
||||
* @param string $destination The destination path. Automatically generated if empty.
|
||||
*/
|
||||
public function writeAlbumCover(
|
||||
Album $album,
|
||||
string $binaryData,
|
||||
string $source,
|
||||
string $extension,
|
||||
string $destination = '',
|
||||
bool $cleanUp = true
|
||||
): void {
|
||||
try {
|
||||
$extension = trim(strtolower($extension), '. ');
|
||||
$destination = $destination ?: $this->generateAlbumCoverPath($extension);
|
||||
$this->imageWriter->writeFromBinaryData($destination, $binaryData);
|
||||
$destination = $destination ?: $this->generateAlbumCoverPath($album, $extension);
|
||||
$this->imageWriter->write($destination, $source);
|
||||
|
||||
if ($cleanUp) {
|
||||
$this->deleteAlbumCoverFiles($album);
|
||||
|
@ -54,26 +54,26 @@ class MediaMetadataService
|
|||
|
||||
public function downloadArtistImage(Artist $artist, string $imageUrl): void
|
||||
{
|
||||
$extension = explode('.', $imageUrl);
|
||||
$this->writeArtistImage($artist, file_get_contents($imageUrl), last($extension));
|
||||
$this->writeArtistImage($artist, $imageUrl, '.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an artist image file with binary data and update the Artist with the new image attribute.
|
||||
*
|
||||
* @param string $source Path, URL, or even binary data. See https://image.intervention.io/v2/api/make.
|
||||
* @param string $destination The destination path. Automatically generated if empty.
|
||||
*/
|
||||
public function writeArtistImage(
|
||||
Artist $artist,
|
||||
string $binaryData,
|
||||
string $source,
|
||||
string $extension,
|
||||
string $destination = '',
|
||||
bool $cleanUp = true
|
||||
): void {
|
||||
try {
|
||||
$extension = trim(strtolower($extension), '. ');
|
||||
$destination = $destination ?: $this->generateArtistImagePath($extension);
|
||||
$this->imageWriter->writeFromBinaryData($destination, $binaryData);
|
||||
$destination = $destination ?: $this->generateArtistImagePath($artist, $extension);
|
||||
$this->imageWriter->write($destination, $source);
|
||||
|
||||
if ($cleanUp && $artist->has_image) {
|
||||
@unlink($artist->image_path);
|
||||
|
@ -85,24 +85,14 @@ class MediaMetadataService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the absolute path for an album cover image.
|
||||
*
|
||||
* @param string $extension The extension of the cover (without dot)
|
||||
*/
|
||||
private function generateAlbumCoverPath(string $extension): string
|
||||
private function generateAlbumCoverPath(Album $album, string $extension): string
|
||||
{
|
||||
return album_cover_path(sprintf('%s.%s', sha1(uniqid()), $extension));
|
||||
return album_cover_path(sprintf('%s.%s', sha1((string) $album->id), trim($extension, '.')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the absolute path for an artist image.
|
||||
*
|
||||
* @param string $extension The extension of the cover (without dot)
|
||||
*/
|
||||
private function generateArtistImagePath($extension): string
|
||||
private function generateArtistImagePath(Artist $artist, string $extension): string
|
||||
{
|
||||
return artist_image_path(sprintf('%s.%s', sha1(uniqid()), $extension));
|
||||
return artist_image_path(sprintf('%s.%s', sha1((string) $artist->id), trim($extension, '.')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,11 +114,7 @@ class MediaMetadataService
|
|||
|
||||
private function createThumbnailForAlbum(Album $album): void
|
||||
{
|
||||
$this->imageWriter->writeFromBinaryData(
|
||||
$album->thumbnail_path,
|
||||
file_get_contents($album->cover_path),
|
||||
['max_width' => 48, 'blur' => 10]
|
||||
);
|
||||
$this->imageWriter->write($album->thumbnail_path, $album->cover_path, ['max_width' => 48, 'blur' => 10]);
|
||||
}
|
||||
|
||||
private function deleteAlbumCoverFiles(Album $album): void
|
||||
|
|
78
app/Services/SpotifyService.php
Normal file
78
app/Services/SpotifyService.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Cache\Repository as Cache;
|
||||
use Illuminate\Support\Arr;
|
||||
use LogicException;
|
||||
use SpotifyWebAPI\Session as SpotifySession;
|
||||
use SpotifyWebAPI\SpotifyWebAPI;
|
||||
|
||||
class SpotifyService
|
||||
{
|
||||
private ?SpotifySession $session;
|
||||
|
||||
public function __construct(private SpotifyWebAPI $client, private Cache $cache)
|
||||
{
|
||||
if (static::enabled()) {
|
||||
$this->session = new SpotifySession(config('koel.spotify.client_id'), config('koel.spotify.client_secret'));
|
||||
$this->client->setOptions(['return_assoc' => true]);
|
||||
$this->client->setAccessToken($this->getAccessToken());
|
||||
}
|
||||
}
|
||||
|
||||
public static function enabled(): bool
|
||||
{
|
||||
return config('koel.spotify.client_id') && config('koel.spotify.client_secret');
|
||||
}
|
||||
|
||||
private function getAccessToken(): string
|
||||
{
|
||||
if (!$this->session) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
if (!$this->cache->has('spotify.access_token')) {
|
||||
$this->session->requestCredentialsToken();
|
||||
$token = $this->session->getAccessToken();
|
||||
$this->cache->put('spotify.access_token', $token, Carbon::now()->addMinutes(59));
|
||||
}
|
||||
|
||||
return $this->cache->get('spotify.access_token');
|
||||
}
|
||||
|
||||
public function tryGetArtistImage(Artist $artist): ?string
|
||||
{
|
||||
if (!static::enabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Arr::get(
|
||||
$this->client->search($artist->name, 'artist', ['limit' => 1]),
|
||||
'artists.items.0.images.0.url'
|
||||
);
|
||||
}
|
||||
|
||||
public function tryGetAlbumCover(Album $album): ?string
|
||||
{
|
||||
if (!static::enabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($album->is_unknown || $album->artist->is_unknown || $album->artist->is_various) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($album->name === Album::UNKNOWN_NAME) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Arr::get(
|
||||
$this->client->search("{$album->artist->name} {$album->name}", 'album', ['limit' => 1]),
|
||||
'albums.items.0.images.0.url'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,11 +8,11 @@ final class AlbumInformation implements Arrayable
|
|||
{
|
||||
use FormatsLastFmText;
|
||||
|
||||
private function __construct(
|
||||
public ?string $url,
|
||||
public ?string $cover,
|
||||
public array $wiki,
|
||||
public array $tracks
|
||||
public function __construct(
|
||||
public ?string $url = null,
|
||||
public ?string $cover = null,
|
||||
public array $wiki = ['summary' => '', 'full' => ''],
|
||||
public array $tracks = []
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,6 @@ final class AlbumInformation implements Arrayable
|
|||
{
|
||||
return new self(
|
||||
url: $data->url,
|
||||
cover: count($data->image) > 3 ? $data->image[3]->{'#text'} : $data->image[0]->{'#text'},
|
||||
wiki: [
|
||||
'summary' => isset($data->wiki) ? self::formatLastFmText($data->wiki->summary) : '',
|
||||
'full' => isset($data->wiki) ? self::formatLastFmText($data->wiki->content) : '',
|
||||
|
|
|
@ -8,15 +8,17 @@ final class ArtistInformation implements Arrayable
|
|||
{
|
||||
use FormatsLastFmText;
|
||||
|
||||
private function __construct(public ?string $url, public ?string $image, public array $bio)
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $url = null,
|
||||
public ?string $image = null,
|
||||
public array $bio = ['summary' => '', 'full' => '']
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromLastFmData(object $data): self
|
||||
{
|
||||
return new self(
|
||||
url: $data->url,
|
||||
image: count($data->image) > 3 ? $data->image[3]->{'#text'} : $data->image[0]->{'#text'},
|
||||
bio: [
|
||||
'summary' => isset($data->bio) ? self::formatLastFmText($data->bio->summary) : '',
|
||||
'full' => isset($data->bio) ? self::formatLastFmText($data->bio->content) : '',
|
||||
|
|
|
@ -32,7 +32,8 @@
|
|||
"webmozart/assert": "^1.10",
|
||||
"laravel/sanctum": "^2.15",
|
||||
"laravel/scout": "^9.4",
|
||||
"nunomaduro/collision": "^6.2"
|
||||
"nunomaduro/collision": "^6.2",
|
||||
"jwilsson/spotify-web-api-php": "^5.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "~1.0",
|
||||
|
|
52
composer.lock
generated
52
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1c1082c46c0ad0c38cbfba8ac6e81733",
|
||||
"content-hash": "0c30334f89353cc94db1ff66167be897",
|
||||
"packages": [
|
||||
{
|
||||
"name": "algolia/algoliasearch-client-php",
|
||||
|
@ -1901,6 +1901,56 @@
|
|||
},
|
||||
"time": "2021-09-22T16:34:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jwilsson/spotify-web-api-php",
|
||||
"version": "5.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jwilsson/spotify-web-api-php.git",
|
||||
"reference": "0d6dc349669c3cf50cf39fe3c226ca438eec0489"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/jwilsson/spotify-web-api-php/zipball/0d6dc349669c3cf50cf39fe3c226ca438eec0489",
|
||||
"reference": "0d6dc349669c3cf50cf39fe3c226ca438eec0489",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"php": "^7.3 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^9.4",
|
||||
"squizlabs/php_codesniffer": "^3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SpotifyWebAPI\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jonathan Wilsson",
|
||||
"email": "jonathan.wilsson@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A PHP wrapper for Spotify's Web API.",
|
||||
"homepage": "https://github.com/jwilsson/spotify-web-api-php",
|
||||
"keywords": [
|
||||
"spotify"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/jwilsson/spotify-web-api-php/issues",
|
||||
"source": "https://github.com/jwilsson/spotify-web-api-php/tree/5.2.0"
|
||||
},
|
||||
"time": "2022-07-16T07:32:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v9.19.0",
|
||||
|
|
|
@ -68,6 +68,21 @@ return [
|
|||
'endpoint' => 'https://ws.audioscrobbler.com/2.0',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Last.FM Integration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| See wiki on how to integrate with Last.FM
|
||||
|
|
||||
*/
|
||||
|
||||
'spotify' => [
|
||||
'client_id' => env('SPOTIFY_CLIENT_ID'),
|
||||
'client_secret' => env('SPOTIFY_CLIENT_SECRET'),
|
||||
],
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CDN
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
href="https://www.last.fm/about/trackmymusic"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
scrobbling
|
||||
</a>.
|
||||
>scrobbling</a>.
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<Btn class="connect" @click.prevent="connect">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Cache, httpService } from '@/services'
|
||||
import { albumStore, artistStore, songStore } from '@/stores'
|
||||
|
||||
export const mediaInfoService = {
|
||||
async fetchForArtist (artist: Artist) {
|
||||
|
@ -8,6 +9,10 @@ export const mediaInfoService = {
|
|||
const info = await httpService.get<ArtistInfo | null>(`artists/${artist.id}/information`)
|
||||
info && Cache.set(cacheKey, info)
|
||||
|
||||
if (info?.image) {
|
||||
artistStore.byId(artist.id)!.image = info.image
|
||||
}
|
||||
|
||||
return info
|
||||
},
|
||||
|
||||
|
@ -18,6 +23,11 @@ export const mediaInfoService = {
|
|||
const info = await httpService.get<AlbumInfo | null>(`albums/${album.id}/information`)
|
||||
info && Cache.set(cacheKey, info)
|
||||
|
||||
if (info?.cover) {
|
||||
albumStore.byId(album.id)!.cover = info.cover
|
||||
songStore.byAlbum(album)!.forEach(song => (song.album_cover = info.cover))
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ interface CommonStoreState {
|
|||
settings: Settings
|
||||
use_i_tunes: boolean
|
||||
use_last_fm: boolean
|
||||
use_spotify: boolean
|
||||
users: User[]
|
||||
use_you_tube: boolean,
|
||||
song_count: number,
|
||||
|
@ -30,6 +31,7 @@ export const commonStore = {
|
|||
settings: {} as Settings,
|
||||
use_i_tunes: false,
|
||||
use_last_fm: false,
|
||||
use_spotify: false,
|
||||
users: [],
|
||||
use_you_tube: false,
|
||||
song_count: 0,
|
||||
|
|
2
resources/assets/js/types.d.ts
vendored
2
resources/assets/js/types.d.ts
vendored
|
@ -123,7 +123,7 @@ interface AlbumTrack {
|
|||
}
|
||||
|
||||
interface AlbumInfo {
|
||||
image: string | null
|
||||
cover: string | null
|
||||
readonly tracks: AlbumTrack[]
|
||||
wiki?: {
|
||||
summary: string
|
||||
|
|
|
@ -3,20 +3,28 @@
|
|||
<head>
|
||||
<title>Authentication successful!</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Perfecto!</h3>
|
||||
<h3>Perfecto!</h3>
|
||||
|
||||
<p>Koel has successfully connected to your Last.fm account and is now restarting for the exciting features.</p>
|
||||
<p>This window will automatically close in 3 seconds.</p>
|
||||
<p>Koel has successfully connected to your Last.fm account and is now restarting for the exciting features.</p>
|
||||
<p>This window will automatically close in 3 seconds.</p>
|
||||
|
||||
<script>
|
||||
window.opener.onbeforeunload = function () {};
|
||||
window.opener.location.reload(false);
|
||||
<script>
|
||||
window.opener.onbeforeunload = function () {
|
||||
}
|
||||
|
||||
window.opener.location.reload(false)
|
||||
|
||||
window.setTimeout(function () {
|
||||
window.close()
|
||||
}, 3000)
|
||||
</script>
|
||||
|
||||
window.setTimeout(function () {
|
||||
window.close();
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -37,7 +37,6 @@ Route::prefix('api')->middleware('api')->group(static function (): void {
|
|||
Route::get('ping', static fn () => null);
|
||||
|
||||
Route::middleware('auth')->group(static function (): void {
|
||||
|
||||
Route::post('broadcasting/auth', static function (Request $request) {
|
||||
$pusher = new Pusher(
|
||||
config('broadcasting.connections.pusher.key'),
|
||||
|
@ -108,7 +107,7 @@ Route::prefix('api')->middleware('api')->group(static function (): void {
|
|||
});
|
||||
});
|
||||
|
||||
// Object-storage (S3) routes
|
||||
// Object-storage (S3) routes
|
||||
Route::middleware('os.auth')->prefix('os/s3')->group(static function (): void {
|
||||
Route::post('song', [S3SongController::class, 'put'])->name('s3.song.put'); // we follow AWS's convention here.
|
||||
Route::delete('song', [S3SongController::class, 'remove'])->name('s3.song.remove'); // and here.
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\Listeners;
|
||||
|
||||
use App\Events\AlbumInformationFetched;
|
||||
use App\Models\Album;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Mockery\MockInterface;
|
||||
use phpmock\mockery\PHPMockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DownloadAlbumCoverTest extends TestCase
|
||||
{
|
||||
/** @var MediaMetadataService|MockInterface */
|
||||
private $mediaMetaDataService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->mediaMetaDataService = self::mock(MediaMetadataService::class);
|
||||
PHPMockery::mock('App\Listeners', 'ini_get')->andReturn(true);
|
||||
}
|
||||
|
||||
public function testHandle(): void
|
||||
{
|
||||
/** @var Album $album */
|
||||
$album = Album::factory()->make(['cover' => null]);
|
||||
$event = new AlbumInformationFetched($album, ['image' => 'https://foo.bar/baz.jpg']);
|
||||
|
||||
$this->mediaMetaDataService
|
||||
->shouldReceive('downloadAlbumCover')
|
||||
->once()
|
||||
->with($album, 'https://foo.bar/baz.jpg');
|
||||
|
||||
event($event);
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\Listeners;
|
||||
|
||||
use App\Events\ArtistInformationFetched;
|
||||
use App\Models\Artist;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Mockery\MockInterface;
|
||||
use phpmock\mockery\PHPMockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DownloadArtistImageTest extends TestCase
|
||||
{
|
||||
/** @var MediaMetadataService|MockInterface */
|
||||
private $mediaMetaDataService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->mediaMetaDataService = self::mock(MediaMetadataService::class);
|
||||
PHPMockery::mock('App\Listeners', 'ini_get')->andReturn(true);
|
||||
}
|
||||
|
||||
public function testHandle(): void
|
||||
{
|
||||
/** @var Artist $artist */
|
||||
$artist = Artist::factory()->make(['image' => null]);
|
||||
$event = new ArtistInformationFetched($artist, ['image' => 'https://foo.bar/baz.jpg']);
|
||||
|
||||
$this->mediaMetaDataService
|
||||
->shouldReceive('downloadArtistImage')
|
||||
->once()
|
||||
->with($artist, 'https://foo.bar/baz.jpg');
|
||||
|
||||
event($event);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ class LastfmServiceTest extends TestCase
|
|||
]);
|
||||
|
||||
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
$info = $api->getArtistInformation($artist->name);
|
||||
$info = $api->getArtistInformation($artist);
|
||||
|
||||
self::assertEquals([
|
||||
'url' => 'https://www.last.fm/music/Kamelot',
|
||||
|
@ -51,7 +51,7 @@ class LastfmServiceTest extends TestCase
|
|||
|
||||
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
|
||||
self::assertNull($api->getArtistInformation($artist->name));
|
||||
self::assertNull($api->getArtistInformation($artist));
|
||||
}
|
||||
|
||||
public function testGetAlbumInformation(): void
|
||||
|
@ -71,7 +71,7 @@ class LastfmServiceTest extends TestCase
|
|||
]);
|
||||
|
||||
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
$info = $api->getAlbumInformation($album->name, $album->artist->name);
|
||||
$info = $api->getAlbumInformation($album);
|
||||
|
||||
self::assertEquals([
|
||||
'url' => 'https://www.last.fm/music/Kamelot/Epica',
|
||||
|
@ -109,6 +109,6 @@ class LastfmServiceTest extends TestCase
|
|||
|
||||
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
|
||||
self::assertNull($api->getAlbumInformation($album->name, $album->artist->name));
|
||||
self::assertNull($api->getAlbumInformation($album));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,14 @@ use App\Services\ImageWriter;
|
|||
use App\Services\MediaMetadataService;
|
||||
use Illuminate\Log\Logger;
|
||||
use Mockery;
|
||||
use Mockery\LegacyMockInterface;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MediaMetadataServiceTest extends TestCase
|
||||
{
|
||||
private $mediaMetadataService;
|
||||
private $imageWriter;
|
||||
private MediaMetadataService $mediaMetadataService;
|
||||
private LegacyMockInterface|ImageWriter|MockInterface $imageWriter;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue