mirror of
https://github.com/koel/koel
synced 2024-11-24 05:03:05 +00:00
feat: rework 3rd integration
This commit is contained in:
parent
f1d33b98e8
commit
f010c773a1
58 changed files with 717 additions and 822 deletions
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Get a URL for static file requests.
|
||||
* If this installation of Koel has a CDN_URL configured, use it as the base.
|
||||
|
@ -38,3 +40,26 @@ function koel_version(): string
|
|||
{
|
||||
return trim(file_get_contents(base_path('.version')));
|
||||
}
|
||||
|
||||
function attempt(callable $callback, bool $log = true): mixed
|
||||
{
|
||||
try {
|
||||
return $callback();
|
||||
} catch (Throwable $e) {
|
||||
if ($log) {
|
||||
Log::error('Failed attempt', ['error' => $e]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function attempt_if($condition, callable $callback, bool $log = true): mixed
|
||||
{
|
||||
return value($condition) ? attempt($callback, $log) : null;
|
||||
}
|
||||
|
||||
function attempt_unless($condition, callable $callback, bool $log = true): mixed
|
||||
{
|
||||
return !value($condition) ? attempt($callback, $log) : null;
|
||||
}
|
||||
|
|
|
@ -21,9 +21,6 @@ class DataController extends Controller
|
|||
|
||||
/** @param User $currentUser */
|
||||
public function __construct(
|
||||
private LastfmService $lastfmService,
|
||||
private YouTubeService $youTubeService,
|
||||
private ITunesService $iTunesService,
|
||||
private MediaCacheService $mediaCacheService,
|
||||
private SettingRepository $settingRepository,
|
||||
private PlaylistRepository $playlistRepository,
|
||||
|
@ -46,9 +43,9 @@ class DataController extends Controller
|
|||
),
|
||||
'users' => $this->currentUser->is_admin ? $this->userRepository->getAll() : [],
|
||||
'currentUser' => $this->currentUser,
|
||||
'useLastfm' => $this->lastfmService->used(),
|
||||
'useYouTube' => $this->youTubeService->enabled(),
|
||||
'useiTunes' => $this->iTunesService->used(),
|
||||
'useLastfm' => LastfmService::used(),
|
||||
'useYouTube' => YouTubeService::enabled(),
|
||||
'useiTunes' => ITunesService::used(),
|
||||
'allowDownload' => config('koel.download.allow'),
|
||||
'supportsTranscoding' => config('koel.streaming.ffmpeg_path')
|
||||
&& is_executable(config('koel.streaming.ffmpeg_path')),
|
||||
|
|
|
@ -22,7 +22,7 @@ class LastfmController extends Controller
|
|||
public function connect()
|
||||
{
|
||||
abort_unless(
|
||||
$this->lastfm->enabled(),
|
||||
LastfmService::enabled(),
|
||||
Response::HTTP_NOT_IMPLEMENTED,
|
||||
'Koel is not configured to use with Last.fm yet.'
|
||||
);
|
||||
|
@ -34,7 +34,7 @@ class LastfmController extends Controller
|
|||
$this->tokenManager->createToken($this->currentUser)->plainTextToken
|
||||
));
|
||||
|
||||
$url = sprintf('https://www.last.fm/api/auth/?api_key=%s&cb=%s', $this->lastfm->getKey(), $callbackUrl);
|
||||
$url = sprintf('https://www.last.fm/api/auth/?api_key=%s&cb=%s', config('koel.lastfm.key'), $callbackUrl);
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@ class DataController extends Controller
|
|||
{
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private LastfmService $lastfmService,
|
||||
private YouTubeService $youTubeService,
|
||||
private ITunesService $iTunesService,
|
||||
private SettingRepository $settingRepository,
|
||||
private PlaylistRepository $playlistRepository,
|
||||
|
@ -34,9 +32,9 @@ class DataController extends Controller
|
|||
return response()->json([
|
||||
'settings' => $this->user->is_admin ? $this->settingRepository->getAllAsKeyValueArray() : [],
|
||||
'playlists' => $this->playlistRepository->getAllByCurrentUser(),
|
||||
'current_user' => UserResource::make($this->user),
|
||||
'use_last_fm' => $this->lastfmService->used(),
|
||||
'use_you_tube' => $this->youTubeService->enabled(), // @todo clean this mess up
|
||||
'current_user' => UserResource::make($this->user, true),
|
||||
'use_last_fm' => LastfmService::used(),
|
||||
'use_you_tube' => YouTubeService::enabled(),
|
||||
'use_i_tunes' => $this->iTunesService->used(),
|
||||
'allow_download' => config('koel.download.allow'),
|
||||
'supports_transcoding' => config('koel.streaming.ffmpeg_path')
|
||||
|
|
|
@ -7,7 +7,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
public function __construct(private User $user)
|
||||
public function __construct(private User $user, private bool $includePreferences = false)
|
||||
{
|
||||
parent::__construct($user);
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ class UserResource extends JsonResource
|
|||
'email' => $this->user->email,
|
||||
'avatar' => $this->user->avatar,
|
||||
'is_admin' => $this->user->is_admin,
|
||||
'preferences' => $this->when($this->includePreferences, $this->user->preferences),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\LastfmService;
|
||||
|
@ -19,25 +18,12 @@ class ScrobbleJob implements ShouldQueue
|
|||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
private User $user;
|
||||
private Song $song;
|
||||
private int $timestamp;
|
||||
|
||||
public function __construct(User $user, Song $song, int $timestamp)
|
||||
public function __construct(public User $user, public Song $song, public int $timestamp)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->song = $song;
|
||||
$this->timestamp = $timestamp;
|
||||
}
|
||||
|
||||
public function handle(LastfmService $lastfmService): void
|
||||
{
|
||||
$lastfmService->scrobble(
|
||||
$this->song->artist->name,
|
||||
$this->song->title,
|
||||
$this->timestamp,
|
||||
$this->song->album->name === Album::UNKNOWN_NAME ? '' : $this->song->album->name,
|
||||
$this->user->lastfm_session_key
|
||||
);
|
||||
$lastfmService->scrobble($this->song, $this->user, $this->timestamp);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,27 +3,17 @@
|
|||
namespace App\Listeners;
|
||||
|
||||
use App\Events\SongsBatchLiked;
|
||||
use App\Models\Song;
|
||||
use App\Services\LastfmService;
|
||||
use App\Values\LastfmLoveTrackParameters;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class LoveMultipleTracksOnLastfm implements ShouldQueue
|
||||
{
|
||||
private LastfmService $lastfm;
|
||||
|
||||
public function __construct(LastfmService $lastfm)
|
||||
public function __construct(private LastfmService $lastfm)
|
||||
{
|
||||
$this->lastfm = $lastfm;
|
||||
}
|
||||
|
||||
public function handle(SongsBatchLiked $event): void
|
||||
{
|
||||
$this->lastfm->batchToggleLoveTracks(
|
||||
$event->songs->map(static function (Song $song): LastfmLoveTrackParameters {
|
||||
return LastfmLoveTrackParameters::make($song->title, $song->artist->name);
|
||||
}),
|
||||
$event->user
|
||||
);
|
||||
$this->lastfm->batchToggleLoveTracks($event->songs, $event->user, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace App\Listeners;
|
|||
|
||||
use App\Events\SongLikeToggled;
|
||||
use App\Services\LastfmService;
|
||||
use App\Values\LastfmLoveTrackParameters;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class LoveTrackOnLastfm implements ShouldQueue
|
||||
|
@ -16,7 +15,7 @@ class LoveTrackOnLastfm implements ShouldQueue
|
|||
public function handle(SongLikeToggled $event): void
|
||||
{
|
||||
if (
|
||||
!$this->lastfm->enabled() ||
|
||||
!LastfmService::enabled() ||
|
||||
!$event->interaction->user->lastfm_session_key ||
|
||||
$event->interaction->song->artist->is_unknown
|
||||
) {
|
||||
|
@ -24,8 +23,8 @@ class LoveTrackOnLastfm implements ShouldQueue
|
|||
}
|
||||
|
||||
$this->lastfm->toggleLoveTrack(
|
||||
LastfmLoveTrackParameters::make($event->interaction->song->title, $event->interaction->song->artist->name),
|
||||
$event->interaction->user->lastfm_session_key,
|
||||
$event->interaction->song,
|
||||
$event->interaction->user,
|
||||
$event->interaction->liked
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,28 +3,17 @@
|
|||
namespace App\Listeners;
|
||||
|
||||
use App\Events\SongsBatchUnliked;
|
||||
use App\Models\Song;
|
||||
use App\Services\LastfmService;
|
||||
use App\Values\LastfmLoveTrackParameters;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class UnloveMultipleTracksOnLastfm implements ShouldQueue
|
||||
{
|
||||
private LastfmService $lastfm;
|
||||
|
||||
public function __construct(LastfmService $lastfm)
|
||||
public function __construct(private LastfmService $lastfm)
|
||||
{
|
||||
$this->lastfm = $lastfm;
|
||||
}
|
||||
|
||||
public function handle(SongsBatchUnliked $event): void
|
||||
{
|
||||
$this->lastfm->batchToggleLoveTracks(
|
||||
$event->songs->map(static function (Song $song): LastfmLoveTrackParameters {
|
||||
return LastfmLoveTrackParameters::make($song->title, $song->artist->name);
|
||||
}),
|
||||
$event->user,
|
||||
false
|
||||
);
|
||||
$this->lastfm->batchToggleLoveTracks($event->songs, $event->user, false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,25 +8,16 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
|
||||
class UpdateLastfmNowPlaying implements ShouldQueue
|
||||
{
|
||||
private LastfmService $lastfm;
|
||||
|
||||
public function __construct(LastfmService $lastfm)
|
||||
public function __construct(private LastfmService $lastfm)
|
||||
{
|
||||
$this->lastfm = $lastfm;
|
||||
}
|
||||
|
||||
public function handle(SongStartedPlaying $event): void
|
||||
{
|
||||
if (!$this->lastfm->enabled() || !$event->user->lastfm_session_key || $event->song->artist->is_unknown) {
|
||||
if (!LastfmService::enabled() || !$event->user->lastfm_session_key || $event->song->artist->is_unknown) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lastfm->updateNowPlaying(
|
||||
$event->song->artist->name,
|
||||
$event->song->title,
|
||||
$event->song->album->is_unknown ? '' : $event->song->album->name,
|
||||
$event->song->length,
|
||||
$event->user->lastfm_session_key
|
||||
);
|
||||
$this->lastfm->updateNowPlaying($event->song, $event->user);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,7 @@ namespace App\Models;
|
|||
|
||||
use App\Facades\Download;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use ZipArchive;
|
||||
|
||||
class SongZipArchive
|
||||
|
@ -43,12 +41,10 @@ class SongZipArchive
|
|||
|
||||
public function addSong(Song $song): self
|
||||
{
|
||||
try {
|
||||
attempt(function () use ($song): void {
|
||||
$path = Download::fromSong($song);
|
||||
$this->archive->addFile($path, $this->generateZipContentFileNameFromPath($path));
|
||||
} catch (Throwable $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
|
@ -3,33 +3,15 @@
|
|||
namespace App\Observers;
|
||||
|
||||
use App\Models\Album;
|
||||
use Illuminate\Log\Logger;
|
||||
use Throwable;
|
||||
|
||||
class AlbumObserver
|
||||
{
|
||||
private Logger $logger;
|
||||
|
||||
public function __construct(Logger $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function deleted(Album $album): void
|
||||
{
|
||||
$this->deleteAlbumCover($album);
|
||||
}
|
||||
|
||||
private function deleteAlbumCover(Album $album): void
|
||||
{
|
||||
if (!$album->has_cover) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
unlink($album->cover_path);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
}
|
||||
attempt(static fn () => unlink($album->cover_path));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\LastfmService;
|
||||
use App\Services\MusicEncyclopedia;
|
||||
use App\Services\NullMusicEncyclopedia;
|
||||
use App\Services\SpotifyService;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
@ -37,6 +40,10 @@ class AppServiceProvider extends ServiceProvider
|
|||
? new SpotifySession(config('koel.spotify.client_id'), config('koel.spotify.client_secret'))
|
||||
: null;
|
||||
});
|
||||
|
||||
$this->app->bind(MusicEncyclopedia::class, function () {
|
||||
return $this->app->get(LastfmService::enabled() ? LastfmService::class : NullMusicEncyclopedia::class);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,7 +5,6 @@ namespace App\Repositories;
|
|||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
abstract class Repository implements RepositoryInterface
|
||||
{
|
||||
|
@ -20,10 +19,7 @@ abstract class Repository implements RepositoryInterface
|
|||
|
||||
// This instantiation may fail during a console command if e.g. APP_KEY is empty,
|
||||
// rendering the whole installation failing.
|
||||
try {
|
||||
$this->auth = app(Guard::class);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
attempt(fn () => $this->auth = app(Guard::class), false);
|
||||
}
|
||||
|
||||
private static function guessModelClass(): string
|
||||
|
|
|
@ -3,19 +3,16 @@
|
|||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Throwable;
|
||||
|
||||
class ImageData implements Rule
|
||||
{
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
try {
|
||||
return attempt(static function () use ($value) {
|
||||
[$header,] = explode(';', $value);
|
||||
|
||||
return (bool) preg_match('/data:image\/(jpe?g|png|webp|gif)/i', $header);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}, false) ?? false;
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
|
|
|
@ -6,7 +6,6 @@ use getID3;
|
|||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Throwable;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class SupportedAudioFile implements Rule
|
||||
|
@ -16,16 +15,14 @@ class SupportedAudioFile implements Rule
|
|||
/** @param UploadedFile $value */
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
try {
|
||||
return attempt(static function () use ($value) {
|
||||
Assert::oneOf(
|
||||
Arr::get((new getID3())->analyze($value->getRealPath()), 'fileformat'),
|
||||
self::SUPPORTED_FORMATS
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}, false) ?? false;
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
|
|
|
@ -4,14 +4,12 @@ namespace App\Rules;
|
|||
|
||||
use App\Values\SmartPlaylistRule;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Throwable;
|
||||
|
||||
class ValidSmartPlaylistRulePayload implements Rule
|
||||
{
|
||||
/** @param array $value */
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
try {
|
||||
return attempt(static function () use ($value) {
|
||||
foreach ((array) $value as $ruleGroupConfig) {
|
||||
foreach ($ruleGroupConfig['rules'] as $rule) {
|
||||
SmartPlaylistRule::assertConfig($rule, false);
|
||||
|
@ -19,9 +17,7 @@ class ValidSmartPlaylistRulePayload implements Rule
|
|||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}, false) ?? false;
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\ApiClients;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Client as GuzzleHttpClient;
|
||||
use GuzzleHttp\Promise\Promise;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Log\Logger;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
use SimpleXMLElement;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @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)
|
||||
* @method object head($uri, array $data = [], bool $appendKey = true)
|
||||
* @method object delete($uri, array $data = [], bool $appendKey = true)
|
||||
* @method mixed get(string $uri, array $data = [], bool $appendKey = true)
|
||||
* @method mixed post($uri, array $data = [], bool $appendKey = true)
|
||||
* @method mixed put($uri, array $data = [], bool $appendKey = true)
|
||||
* @method mixed patch($uri, array $data = [], bool $appendKey = true)
|
||||
* @method mixed head($uri, array $data = [], bool $appendKey = true)
|
||||
* @method mixed delete($uri, array $data = [], bool $appendKey = true)
|
||||
* @method Promise getAsync(string $uri, array $data = [], bool $appendKey = true)
|
||||
* @method Promise postAsync($uri, array $data = [], bool $appendKey = true)
|
||||
* @method Promise putAsync($uri, array $data = [], bool $appendKey = true)
|
||||
|
@ -44,9 +41,6 @@ abstract class ApiClient
|
|||
];
|
||||
|
||||
protected string $responseFormat = 'json';
|
||||
protected Client $client;
|
||||
protected Cache $cache;
|
||||
protected Logger $logger;
|
||||
|
||||
/**
|
||||
* The query parameter name for the key.
|
||||
|
@ -55,11 +49,8 @@ abstract class ApiClient
|
|||
*/
|
||||
protected string $keyParam = 'key';
|
||||
|
||||
public function __construct(Client $client, Cache $cache, Logger $logger)
|
||||
public function __construct(protected GuzzleHttpClient $wrapped)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->cache = $cache;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,12 +63,11 @@ abstract class ApiClient
|
|||
* an "API signature" of the request. Appending an API key will break the request.
|
||||
* @param array $params An array of parameters
|
||||
*
|
||||
* @return mixed|SimpleXMLElement|void
|
||||
*/
|
||||
public function request(string $method, string $uri, bool $appendKey = true, array $params = []) // @phpcs:ignore
|
||||
public function request(string $method, string $uri, bool $appendKey = true, array $params = []): mixed
|
||||
{
|
||||
try {
|
||||
$body = (string) $this->getClient()
|
||||
return attempt(function () use ($method, $uri, $appendKey, $params) {
|
||||
$body = (string) $this->wrapped
|
||||
->$method($this->buildUrl($uri, $appendKey), ['form_params' => $params])
|
||||
->getBody();
|
||||
|
||||
|
@ -90,14 +80,12 @@ abstract class ApiClient
|
|||
}
|
||||
|
||||
return $body;
|
||||
} catch (ClientException $e) {
|
||||
$this->logger->error($e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function requestAsync(string $method, string $uri, bool $appendKey = true, array $params = []): Promise
|
||||
{
|
||||
return $this->getClient()->$method($this->buildUrl($uri, $appendKey), ['form_params' => $params]);
|
||||
return $this->wrapped->$method($this->buildUrl($uri, $appendKey), ['form_params' => $params]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -155,11 +143,6 @@ abstract class ApiClient
|
|||
return $uri;
|
||||
}
|
||||
|
||||
public function getClient(): Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
abstract public function getKey(): ?string;
|
||||
|
||||
abstract public function getSecret(): ?string;
|
21
app/Services/ApiClients/ITunesClient.php
Normal file
21
app/Services/ApiClients/ITunesClient.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\ApiClients;
|
||||
|
||||
class ITunesClient extends ApiClient
|
||||
{
|
||||
public function getKey(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSecret(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getEndpoint(): ?string
|
||||
{
|
||||
return config('koel.itunes.endpoint');
|
||||
}
|
||||
}
|
95
app/Services/ApiClients/LastfmClient.php
Normal file
95
app/Services/ApiClients/LastfmClient.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\ApiClients;
|
||||
|
||||
use GuzzleHttp\Promise\Promise;
|
||||
|
||||
class LastfmClient extends ApiClient
|
||||
{
|
||||
protected string $keyParam = 'api_key';
|
||||
|
||||
public function post($uri, array $data = [], bool $appendKey = true): mixed
|
||||
{
|
||||
return parent::post($uri, $this->buildAuthCallParams($data), $appendKey);
|
||||
}
|
||||
|
||||
public function postAsync($uri, array $data = [], bool $appendKey = true): Promise
|
||||
{
|
||||
return parent::postAsync($uri, $this->buildAuthCallParams($data), $appendKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Last.fm's session key for the authenticated user using a token.
|
||||
*
|
||||
* @param string $token The token after successfully connecting to Last.fm
|
||||
*
|
||||
* @see http://www.last.fm/api/webauth#4
|
||||
*/
|
||||
public function getSessionKey(string $token): ?string
|
||||
{
|
||||
$query = $this->buildAuthCallParams([
|
||||
'method' => 'auth.getSession',
|
||||
'token' => $token,
|
||||
], true);
|
||||
|
||||
|
||||
return attempt(fn () => $this->get("/?$query&format=json", [], false)->session->key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the parameters to use for _authenticated_ Last.fm API calls.
|
||||
* Such calls require:
|
||||
* - The API key (api_key)
|
||||
* - The API signature (api_sig).
|
||||
*
|
||||
* @see http://www.last.fm/api/webauth#5
|
||||
*
|
||||
* @param array $params The array of parameters
|
||||
* @param bool $toString Whether to turn the array into a query string
|
||||
*
|
||||
* @return array<mixed>|string
|
||||
*/
|
||||
private function buildAuthCallParams(array $params, bool $toString = false): array|string
|
||||
{
|
||||
$params['api_key'] = $this->getKey();
|
||||
ksort($params);
|
||||
|
||||
// Generate the API signature.
|
||||
// @link http://www.last.fm/api/webauth#6
|
||||
$str = '';
|
||||
|
||||
foreach ($params as $name => $value) {
|
||||
$str .= $name . $value;
|
||||
}
|
||||
|
||||
$str .= $this->getSecret();
|
||||
$params['api_sig'] = md5($str);
|
||||
|
||||
if (!$toString) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$query = '';
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$query .= "$key=$value&";
|
||||
}
|
||||
|
||||
return rtrim($query, '&');
|
||||
}
|
||||
|
||||
public function getKey(): ?string
|
||||
{
|
||||
return config('koel.lastfm.key');
|
||||
}
|
||||
|
||||
public function getEndpoint(): ?string
|
||||
{
|
||||
return config('koel.lastfm.endpoint');
|
||||
}
|
||||
|
||||
public function getSecret(): ?string
|
||||
{
|
||||
return config('koel.lastfm.secret');
|
||||
}
|
||||
}
|
|
@ -1,33 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\ApiClients;
|
||||
|
||||
use App\Exceptions\SpotifyIntegrationDisabledException;
|
||||
use App\Services\SpotifyService;
|
||||
use Illuminate\Cache\Repository as Cache;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use SpotifyWebAPI\Session;
|
||||
use SpotifyWebAPI\SpotifyWebAPI;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @method array search(string $keywords, string|array $type, array|object $options = [])
|
||||
*/
|
||||
class SpotifyClient
|
||||
{
|
||||
public function __construct(
|
||||
public SpotifyWebAPI $wrapped,
|
||||
private ?Session $session,
|
||||
private Cache $cache,
|
||||
private LoggerInterface $log
|
||||
) {
|
||||
public function __construct(public SpotifyWebAPI $wrapped, private ?Session $session, private Cache $cache)
|
||||
{
|
||||
if (SpotifyService::enabled()) {
|
||||
$this->wrapped->setOptions(['return_assoc' => true]);
|
||||
|
||||
try {
|
||||
$this->setAccessToken();
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error('Failed to set Spotify access token', ['exception' => $e]);
|
||||
}
|
||||
attempt(fn () => $this->setAccessToken());
|
||||
}
|
||||
}
|
||||
|
21
app/Services/ApiClients/YouTubeClient.php
Normal file
21
app/Services/ApiClients/YouTubeClient.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\ApiClients;
|
||||
|
||||
class YouTubeClient extends ApiClient
|
||||
{
|
||||
public function getKey(): ?string
|
||||
{
|
||||
return config('koel.youtube.key');
|
||||
}
|
||||
|
||||
public function getSecret(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getEndpoint(): ?string
|
||||
{
|
||||
return config('koel.youtube.endpoint');
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
interface ApiConsumerInterface
|
||||
{
|
||||
public function getEndpoint(): ?string;
|
||||
|
||||
public function getKey(): ?string;
|
||||
|
||||
public function getSecret(): ?string;
|
||||
}
|
|
@ -4,14 +4,10 @@ namespace App\Services;
|
|||
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Log\Logger;
|
||||
use Throwable;
|
||||
|
||||
class ApplicationInformationService
|
||||
{
|
||||
private const CACHE_KEY = 'latestKoelVersion';
|
||||
|
||||
public function __construct(private Client $client, private Cache $cache, private Logger $logger)
|
||||
public function __construct(private Client $client, private Cache $cache)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -20,15 +16,11 @@ class ApplicationInformationService
|
|||
*/
|
||||
public function getLatestVersionNumber(): string
|
||||
{
|
||||
return $this->cache->remember(self::CACHE_KEY, now()->addDay(), function (): string {
|
||||
try {
|
||||
return attempt(function () {
|
||||
return $this->cache->remember('latestKoelVersion', now()->addDay(), function (): string {
|
||||
return json_decode($this->client->get('https://api.github.com/repos/koel/koel/tags')->getBody())[0]
|
||||
->name;
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
|
||||
return koel_version();
|
||||
}
|
||||
});
|
||||
});
|
||||
}) ?? koel_version();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ use Illuminate\Contracts\Cache\Repository as Cache;
|
|||
use Illuminate\Support\Arr;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Throwable;
|
||||
|
||||
class FileSynchronizer
|
||||
{
|
||||
|
@ -101,7 +100,7 @@ class FileSynchronizer
|
|||
*/
|
||||
private function tryGenerateAlbumCover(Album $album, ?array $coverData): void
|
||||
{
|
||||
try {
|
||||
attempt(function () use ($album, $coverData): void {
|
||||
// If the album has no cover, we try to get the cover image from existing tag data
|
||||
if ($coverData) {
|
||||
$extension = explode('/', $coverData['image_mime']);
|
||||
|
@ -119,8 +118,7 @@ class FileSynchronizer
|
|||
$extension = pathinfo($cover, PATHINFO_EXTENSION);
|
||||
$this->mediaMetadataService->writeAlbumCover($album, $cover, $extension);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,11 +150,7 @@ class FileSynchronizer
|
|||
|
||||
private static function isImage(string $path): bool
|
||||
{
|
||||
try {
|
||||
return (bool) exif_imagetype($path);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
return attempt(static fn () => (bool) exif_imagetype($path)) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Services;
|
||||
|
||||
use SplFileInfo;
|
||||
use Throwable;
|
||||
|
||||
class Helper
|
||||
{
|
||||
|
@ -21,11 +20,6 @@ class Helper
|
|||
$file = is_string($file) ? new SplFileInfo($file) : $file;
|
||||
|
||||
// Workaround for #344, where getMTime() fails for certain files with Unicode names on Windows.
|
||||
try {
|
||||
return $file->getMTime();
|
||||
} catch (Throwable) {
|
||||
// Just use current stamp for mtime.
|
||||
return time();
|
||||
}
|
||||
return attempt(static fn () => $file->getMTime()) ?? time();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,19 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use Throwable;
|
||||
use App\Services\ApiClients\ITunesClient;
|
||||
use Illuminate\Cache\Repository as Cache;
|
||||
|
||||
class ITunesService extends ApiClient implements ApiConsumerInterface
|
||||
class ITunesService
|
||||
{
|
||||
public function __construct(private ITunesClient $client, private Cache $cache)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to use iTunes services.
|
||||
*/
|
||||
public function used(): bool
|
||||
public static function used(): bool
|
||||
{
|
||||
return (bool) config('koel.itunes.enabled');
|
||||
}
|
||||
|
@ -23,7 +28,7 @@ class ITunesService extends ApiClient implements ApiConsumerInterface
|
|||
*/
|
||||
public function getTrackUrl(string $term, string $album = '', string $artist = ''): ?string
|
||||
{
|
||||
try {
|
||||
return attempt(function () use ($term, $album, $artist): ?string {
|
||||
return $this->cache->remember(
|
||||
md5("itunes_track_url_$term$album$artist"),
|
||||
24 * 60 * 7,
|
||||
|
@ -35,9 +40,7 @@ class ITunesService extends ApiClient implements ApiConsumerInterface
|
|||
'limit' => 1,
|
||||
];
|
||||
|
||||
$response = json_decode(
|
||||
$this->getClient()->get($this->getEndpoint(), ['query' => $params])->getBody()
|
||||
);
|
||||
$response = $this->client->get('/', ['query' => $params]);
|
||||
|
||||
if (!$response->resultCount) {
|
||||
return null;
|
||||
|
@ -45,28 +48,10 @@ class ITunesService extends ApiClient implements ApiConsumerInterface
|
|||
|
||||
$trackUrl = $response->results[0]->trackViewUrl;
|
||||
$connector = parse_url($trackUrl, PHP_URL_QUERY) ? '&' : '?';
|
||||
|
||||
return $trackUrl . "{$connector}at=" . config('koel.itunes.affiliate_id');
|
||||
}
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getKey(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSecret(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getEndpoint(): ?string
|
||||
{
|
||||
return config('koel.itunes.endpoint');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,257 +4,126 @@ namespace App\Services;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\ApiClients\LastfmClient;
|
||||
use App\Values\AlbumInformation;
|
||||
use App\Values\ArtistInformation;
|
||||
use App\Values\LastfmLoveTrackParameters;
|
||||
use GuzzleHttp\Promise\Promise;
|
||||
use GuzzleHttp\Promise\Utils;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class LastfmService extends ApiClient implements ApiConsumerInterface
|
||||
class LastfmService implements MusicEncyclopedia
|
||||
{
|
||||
/**
|
||||
* Override the key param, since, again, Last.fm wants to be different.
|
||||
*/
|
||||
protected string $keyParam = 'api_key';
|
||||
public function __construct(private LastfmClient $client)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if our application is using Last.fm.
|
||||
*/
|
||||
public function used(): bool
|
||||
public static function used(): bool
|
||||
{
|
||||
return (bool) $this->getKey();
|
||||
return (bool) config('koel.lastfm.key');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if Last.fm integration is enabled.
|
||||
*/
|
||||
public function enabled(): bool
|
||||
public static function enabled(): bool
|
||||
{
|
||||
return $this->getKey() && $this->getSecret();
|
||||
return config('koel.lastfm.key') && config('koel.lastfm.secret');
|
||||
}
|
||||
|
||||
public function getArtistInformation(Artist $artist): ?ArtistInformation
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return null;
|
||||
}
|
||||
return attempt_if(static::enabled(), function () use ($artist): ?ArtistInformation {
|
||||
$name = urlencode($artist->name);
|
||||
$response = $this->client->get("?method=artist.getInfo&autocorrect=1&artist=$name&format=json");
|
||||
|
||||
$name = urlencode($artist->name);
|
||||
|
||||
try {
|
||||
return $this->cache->remember(
|
||||
md5("lastfm_artist_$name"),
|
||||
now()->addWeek(),
|
||||
function () use ($name): ?ArtistInformation {
|
||||
$response = $this->get("?method=artist.getInfo&autocorrect=1&artist=$name&format=json");
|
||||
|
||||
return $response?->artist ? ArtistInformation::fromLastFmData($response->artist) : null;
|
||||
}
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
|
||||
return null;
|
||||
}
|
||||
return $response?->artist ? ArtistInformation::fromLastFmData($response->artist) : null;
|
||||
});
|
||||
}
|
||||
|
||||
public function getAlbumInformation(Album $album): ?AlbumInformation
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return null;
|
||||
}
|
||||
return attempt_if(static::enabled(), function () use ($album): ?AlbumInformation {
|
||||
$albumName = urlencode($album->name);
|
||||
$artistName = urlencode($album->artist->name);
|
||||
|
||||
$albumName = urlencode($album->name);
|
||||
$artistName = urlencode($album->artist->name);
|
||||
$response = $this->client
|
||||
->get("?method=album.getInfo&autocorrect=1&album=$albumName&artist=$artistName&format=json");
|
||||
|
||||
try {
|
||||
$cacheKey = md5("lastfm_album_{$albumName}_{$artistName}");
|
||||
|
||||
return $this->cache->remember(
|
||||
$cacheKey,
|
||||
now()->addWeek(),
|
||||
function () use ($albumName, $artistName): ?AlbumInformation {
|
||||
$response = $this
|
||||
->get("?method=album.getInfo&autocorrect=1&album=$albumName&artist=$artistName&format=json");
|
||||
|
||||
return $response?->album ? AlbumInformation::fromLastFmData($response->album) : null;
|
||||
}
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
|
||||
return null;
|
||||
}
|
||||
return $response?->album ? AlbumInformation::fromLastFmData($response->album) : null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Last.fm's session key for the authenticated user using a token.
|
||||
*
|
||||
* @param string $token The token after successfully connecting to Last.fm
|
||||
*
|
||||
* @see http://www.last.fm/api/webauth#4
|
||||
*/
|
||||
public function getSessionKey(string $token): ?string
|
||||
public function scrobble(Song $song, User $user, int $timestamp): void
|
||||
{
|
||||
$query = $this->buildAuthCallParams([
|
||||
'method' => 'auth.getSession',
|
||||
'token' => $token,
|
||||
], true);
|
||||
|
||||
try {
|
||||
return $this->get("/?$query&format=json", [], false)->session->key;
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function scrobble(
|
||||
string $artistName,
|
||||
string $trackName,
|
||||
$timestamp,
|
||||
string $albumName,
|
||||
string $sessionKey
|
||||
): void {
|
||||
$params = [
|
||||
'artist' => $artistName,
|
||||
'track' => $trackName,
|
||||
'artist' => $song->artist->name,
|
||||
'track' => $song->title,
|
||||
'timestamp' => $timestamp,
|
||||
'sk' => $sessionKey,
|
||||
'sk' => $user->lastfm_session_key,
|
||||
'method' => 'track.scrobble',
|
||||
];
|
||||
|
||||
if ($albumName) {
|
||||
$params['album'] = $albumName;
|
||||
if ($song->album->name !== Album::UNKNOWN_NAME) {
|
||||
$params['album'] = $song->album->name;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->post('/', $this->buildAuthCallParams($params), false);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
}
|
||||
attempt(fn () => $this->client->post('/', $params, false));
|
||||
}
|
||||
|
||||
public function toggleLoveTrack(LastfmLoveTrackParameters $params, string $sessionKey, bool $love = true): void
|
||||
public function toggleLoveTrack(Song $song, User $user, bool $love): void
|
||||
{
|
||||
try {
|
||||
$this->post('/', $this->buildAuthCallParams([
|
||||
'track' => $params->trackName,
|
||||
'artist' => $params->artistName,
|
||||
'sk' => $sessionKey,
|
||||
'method' => $love ? 'track.love' : 'track.unlove',
|
||||
]), false);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
}
|
||||
attempt(fn () => $this->client->post('/', [
|
||||
'track' => $song->title,
|
||||
'artist' => $song->artist->name,
|
||||
'sk' => $user->lastfm_session_key,
|
||||
'method' => $love ? 'track.love' : 'track.unlove',
|
||||
], false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection|array<LastfmLoveTrackParameters> $parameterCollection
|
||||
* @param Collection|array<array-key, Song> $songs
|
||||
*/
|
||||
public function batchToggleLoveTracks(Collection $parameterCollection, string $sessionKey, bool $love = true): void
|
||||
public function batchToggleLoveTracks(Collection $songs, User $user, bool $love): void
|
||||
{
|
||||
$promises = $parameterCollection->map(
|
||||
fn (LastfmLoveTrackParameters $params): Promise => $this->postAsync('/', $this->buildAuthCallParams([
|
||||
'track' => $params->trackName,
|
||||
'artist' => $params->artistName,
|
||||
'sk' => $sessionKey,
|
||||
'method' => $love ? 'track.love' : 'track.unlove',
|
||||
]), false)
|
||||
$promises = $songs->map(
|
||||
function (Song $song) use ($user, $love): Promise {
|
||||
return $this->client->postAsync('/', [
|
||||
'track' => $song->title,
|
||||
'artist' => $song->artist->name,
|
||||
'sk' => $user->lastfm_session_key,
|
||||
'method' => $love ? 'track.love' : 'track.unlove',
|
||||
], false);
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
Utils::unwrap($promises);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
}
|
||||
attempt(static fn () => Utils::unwrap($promises));
|
||||
}
|
||||
|
||||
public function updateNowPlaying(
|
||||
string $artistName,
|
||||
string $trackName,
|
||||
string $albumName,
|
||||
int|float $duration,
|
||||
string $sessionKey
|
||||
): void {
|
||||
public function updateNowPlaying(Song $song, User $user): void
|
||||
{
|
||||
$params = [
|
||||
'artist' => $artistName,
|
||||
'track' => $trackName,
|
||||
'duration' => $duration,
|
||||
'sk' => $sessionKey,
|
||||
'artist' => $song->artist->name,
|
||||
'track' => $song->title,
|
||||
'duration' => $song->length,
|
||||
'sk' => $user->lastfm_session_key,
|
||||
'method' => 'track.updateNowPlaying',
|
||||
];
|
||||
|
||||
if ($albumName) {
|
||||
$params['album'] = $albumName;
|
||||
if ($song->album->name !== Album::UNKNOWN_NAME) {
|
||||
$params['album'] = $song->album->name;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->post('/', $this->buildAuthCallParams($params), false);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
}
|
||||
attempt(fn () => $this->client->post('/', $params, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the parameters to use for _authenticated_ Last.fm API calls.
|
||||
* Such calls require:
|
||||
* - The API key (api_key)
|
||||
* - The API signature (api_sig).
|
||||
*
|
||||
* @see http://www.last.fm/api/webauth#5
|
||||
*
|
||||
* @param array $params The array of parameters
|
||||
* @param bool $toString Whether to turn the array into a query string
|
||||
*
|
||||
* @return array<mixed>|string
|
||||
*/
|
||||
public function buildAuthCallParams(array $params, bool $toString = false): array|string
|
||||
public function getSessionKey(string $token): ?string
|
||||
{
|
||||
$params['api_key'] = $this->getKey();
|
||||
ksort($params);
|
||||
|
||||
// Generate the API signature.
|
||||
// @link http://www.last.fm/api/webauth#6
|
||||
$str = '';
|
||||
|
||||
foreach ($params as $name => $value) {
|
||||
$str .= $name . $value;
|
||||
}
|
||||
|
||||
$str .= $this->getSecret();
|
||||
$params['api_sig'] = md5($str);
|
||||
|
||||
if (!$toString) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$query = '';
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$query .= "$key=$value&";
|
||||
}
|
||||
|
||||
return rtrim($query, '&');
|
||||
}
|
||||
|
||||
public function getKey(): ?string
|
||||
{
|
||||
return config('koel.lastfm.key');
|
||||
}
|
||||
|
||||
public function getEndpoint(): ?string
|
||||
{
|
||||
return config('koel.lastfm.endpoint');
|
||||
}
|
||||
|
||||
public function getSecret(): ?string
|
||||
{
|
||||
return config('koel.lastfm.secret');
|
||||
return $this->client->getSessionKey($token);
|
||||
}
|
||||
|
||||
public function setUserSessionKey(User $user, ?string $sessionKey): void
|
||||
|
|
|
@ -6,13 +6,14 @@ use App\Models\Album;
|
|||
use App\Models\Artist;
|
||||
use App\Values\AlbumInformation;
|
||||
use App\Values\ArtistInformation;
|
||||
use Throwable;
|
||||
use Illuminate\Cache\Repository as Cache;
|
||||
|
||||
class MediaInformationService
|
||||
{
|
||||
public function __construct(
|
||||
private LastfmService $lastfmService,
|
||||
private MediaMetadataService $mediaMetadataService
|
||||
private MusicEncyclopedia $encyclopedia,
|
||||
private MediaMetadataService $mediaMetadataService,
|
||||
private Cache $cache
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -22,35 +23,41 @@ class MediaInformationService
|
|||
return null;
|
||||
}
|
||||
|
||||
$info = $this->lastfmService->getAlbumInformation($album) ?: AlbumInformation::make();
|
||||
|
||||
if (!$album->has_cover) {
|
||||
try {
|
||||
$this->mediaMetadataService->tryDownloadAlbumCover($album);
|
||||
$info->cover = $album->cover;
|
||||
} catch (Throwable) {
|
||||
}
|
||||
if ($this->cache->has('album.info.' . $album->id)) {
|
||||
return $this->cache->get('album.info.' . $album->id);
|
||||
}
|
||||
|
||||
$info = $this->encyclopedia->getAlbumInformation($album) ?: AlbumInformation::make();
|
||||
|
||||
attempt_unless($album->has_cover, function () use ($info, $album): void {
|
||||
$this->mediaMetadataService->tryDownloadAlbumCover($album);
|
||||
$info->cover = $album->cover;
|
||||
});
|
||||
|
||||
$this->cache->put('album.info.' . $album->id, $info, now()->addWeek());
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
public function getArtistInformation(Artist $artist): ?ArtistInformation
|
||||
{
|
||||
if ($artist->is_unknown) {
|
||||
if ($artist->is_unknown || $artist->is_various) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$info = $this->lastfmService->getArtistInformation($artist) ?: ArtistInformation::make();
|
||||
|
||||
if (!$artist->has_image) {
|
||||
try {
|
||||
$this->mediaMetadataService->tryDownloadArtistImage($artist);
|
||||
$info->image = $artist->image;
|
||||
} catch (Throwable) {
|
||||
}
|
||||
if ($this->cache->has('artist.info.' . $artist->id)) {
|
||||
return $this->cache->get('artist.info.' . $artist->id);
|
||||
}
|
||||
|
||||
$info = $this->encyclopedia->getArtistInformation($artist) ?: ArtistInformation::make();
|
||||
|
||||
attempt_unless($artist->has_image, function () use ($artist, $info): void {
|
||||
$this->mediaMetadataService->tryDownloadArtistImage($artist);
|
||||
$info->image = $artist->image;
|
||||
});
|
||||
|
||||
$this->cache->put('artist.info.' . $artist->id, $info, now()->addWeek());
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,16 +5,11 @@ namespace App\Services;
|
|||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
class MediaMetadataService
|
||||
{
|
||||
public function __construct(
|
||||
private SpotifyService $spotifyService,
|
||||
private ImageWriter $imageWriter,
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
public function __construct(private SpotifyService $spotifyService, private ImageWriter $imageWriter)
|
||||
{
|
||||
}
|
||||
|
||||
public function tryDownloadAlbumCover(Album $album): void
|
||||
|
@ -37,7 +32,7 @@ class MediaMetadataService
|
|||
?string $destination = '',
|
||||
bool $cleanUp = true
|
||||
): void {
|
||||
try {
|
||||
attempt(function () use ($album, $source, $extension, $destination, $cleanUp): void {
|
||||
$extension = trim(strtolower($extension), '. ');
|
||||
$destination = $destination ?: $this->generateAlbumCoverPath($extension);
|
||||
$this->imageWriter->write($destination, $source);
|
||||
|
@ -48,9 +43,7 @@ class MediaMetadataService
|
|||
|
||||
$album->update(['cover' => basename($destination)]);
|
||||
$this->createThumbnailForAlbum($album);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function tryDownloadArtistImage(Artist $artist): void
|
||||
|
@ -73,7 +66,7 @@ class MediaMetadataService
|
|||
?string $destination = '',
|
||||
bool $cleanUp = true
|
||||
): void {
|
||||
try {
|
||||
attempt(function () use ($artist, $source, $extension, $destination, $cleanUp): void {
|
||||
$extension = trim(strtolower($extension), '. ');
|
||||
$destination = $destination ?: $this->generateArtistImagePath($extension);
|
||||
$this->imageWriter->write($destination, $source);
|
||||
|
@ -83,9 +76,7 @@ class MediaMetadataService
|
|||
}
|
||||
|
||||
$artist->update(['image' => basename($destination)]);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function generateAlbumCoverPath(string $extension): string
|
||||
|
|
15
app/Services/MusicEncyclopedia.php
Normal file
15
app/Services/MusicEncyclopedia.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Values\AlbumInformation;
|
||||
use App\Values\ArtistInformation;
|
||||
|
||||
interface MusicEncyclopedia
|
||||
{
|
||||
public function getArtistInformation(Artist $artist): ?ArtistInformation;
|
||||
|
||||
public function getAlbumInformation(Album $album): ?AlbumInformation;
|
||||
}
|
21
app/Services/NullMusicEncyclopedia.php
Normal file
21
app/Services/NullMusicEncyclopedia.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Values\AlbumInformation;
|
||||
use App\Values\ArtistInformation;
|
||||
|
||||
class NullMusicEncyclopedia implements MusicEncyclopedia
|
||||
{
|
||||
public function getArtistInformation(Artist $artist): ?ArtistInformation
|
||||
{
|
||||
return ArtistInformation::make();
|
||||
}
|
||||
|
||||
public function getAlbumInformation(Album $album): ?AlbumInformation
|
||||
{
|
||||
return AlbumInformation::make();
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ namespace App\Services;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Services\ApiClients\SpotifyClient;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class SpotifyService
|
||||
|
|
|
@ -22,13 +22,13 @@ class PhpStreamer extends Streamer implements DirectStreamerInterface
|
|||
$rangeSet = RangeSet::createFromHeader(get_request_header('Range'));
|
||||
$resource = new FileResource($this->song->path);
|
||||
(new ResourceServlet($resource))->sendResource($rangeSet);
|
||||
} catch (InvalidRangeHeaderException $e) {
|
||||
} catch (InvalidRangeHeaderException) {
|
||||
abort(Response::HTTP_BAD_REQUEST);
|
||||
} catch (UnsatisfiableRangeException $e) {
|
||||
} catch (UnsatisfiableRangeException) {
|
||||
abort(Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
} catch (NonExistentFileException $e) {
|
||||
} catch (NonExistentFileException) {
|
||||
abort(Response::HTTP_NOT_FOUND);
|
||||
} catch (UnreadableFileException $e) {
|
||||
} catch (UnreadableFileException) {
|
||||
abort(Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
} catch (SendFileFailureException $e) {
|
||||
abort_unless(headers_sent(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
|
|
|
@ -3,16 +3,21 @@
|
|||
namespace App\Services;
|
||||
|
||||
use App\Models\Song;
|
||||
use Throwable;
|
||||
use App\Services\ApiClients\YouTubeClient;
|
||||
use Illuminate\Cache\Repository as Cache;
|
||||
|
||||
class YouTubeService extends ApiClient implements ApiConsumerInterface
|
||||
class YouTubeService
|
||||
{
|
||||
public function __construct(private YouTubeClient $client, private Cache $cache)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if our application is using YouTube.
|
||||
*/
|
||||
public function enabled(): bool
|
||||
public static function enabled(): bool
|
||||
{
|
||||
return (bool) $this->getKey();
|
||||
return (bool) config('koel.youtube.key');
|
||||
}
|
||||
|
||||
public function searchVideosRelatedToSong(Song $song, string $pageToken = '') // @phpcs:ignore
|
||||
|
@ -35,40 +40,17 @@ class YouTubeService extends ApiClient implements ApiConsumerInterface
|
|||
* @param int $perPage Number of results per page
|
||||
*
|
||||
*/
|
||||
public function search(string $q, string $pageToken = '', int $perPage = 10) // @phpcs:ignore
|
||||
private function search(string $q, string $pageToken = '', int $perPage = 10) // @phpcs:ignore
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return null;
|
||||
}
|
||||
return attempt_if(static::enabled(), function () use ($q, $pageToken, $perPage) {
|
||||
$uri = sprintf(
|
||||
'search?part=snippet&type=video&maxResults=%s&pageToken=%s&q=%s',
|
||||
$perPage,
|
||||
urlencode($pageToken),
|
||||
urlencode($q)
|
||||
);
|
||||
|
||||
$uri = sprintf(
|
||||
'search?part=snippet&type=video&maxResults=%s&pageToken=%s&q=%s',
|
||||
$perPage,
|
||||
urlencode($pageToken),
|
||||
urlencode($q)
|
||||
);
|
||||
|
||||
try {
|
||||
return $this->cache->remember(md5("youtube_$uri"), 60 * 24 * 7, fn () => $this->get($uri));
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getEndpoint(): ?string
|
||||
{
|
||||
return config('koel.youtube.endpoint');
|
||||
}
|
||||
|
||||
public function getKey(): ?string
|
||||
{
|
||||
return config('koel.youtube.key');
|
||||
}
|
||||
|
||||
public function getSecret(): ?string
|
||||
{
|
||||
return null;
|
||||
return $this->cache->remember(md5("youtube_$uri"), now()->addWeek(), fn () => $this->client->get($uri));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ namespace App\Values;
|
|||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
final class SmartPlaylistRuleGroup implements Arrayable
|
||||
{
|
||||
|
@ -15,11 +14,7 @@ final class SmartPlaylistRuleGroup implements Arrayable
|
|||
|
||||
public static function tryCreate(array $jsonArray): ?self
|
||||
{
|
||||
try {
|
||||
return self::create($jsonArray);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
return attempt(static fn () => self::create($jsonArray));
|
||||
}
|
||||
|
||||
public static function create(array $jsonArray): self
|
||||
|
|
|
@ -9,12 +9,11 @@ class ConvertUserPreferencesFromArrayToJson extends Migration
|
|||
public function up(): void
|
||||
{
|
||||
User::all()->each(static function (User $user): void {
|
||||
try {
|
||||
attempt(static function () use ($user): void {
|
||||
$preferences = unserialize($user->getRawOriginal('preferences'));
|
||||
$user->preferences->lastFmSessionKey = Arr::get($preferences, 'lastfm_session_key');
|
||||
$user->save();
|
||||
} catch (Throwable $exception) {
|
||||
}
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,26 +5,22 @@ use Illuminate\Database\Schema\Blueprint;
|
|||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('songs', static function (Blueprint $table): void {
|
||||
// This migration is actually to fix a mistake that the original one was deleted.
|
||||
// Therefore, we just "try" it and ignore on error.
|
||||
try {
|
||||
if (Schema::hasColumn('songs', 'contributing_artist_id')) {
|
||||
Schema::disableForeignKeyConstraints();
|
||||
attempt_if(Schema::hasColumn('songs', 'contributing_artist_id'), static function () use ($table): void {
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
if (DB::getDriverName() !== 'sqlite') { // @phpstan-ignore-line
|
||||
$table->dropForeign('songs_contributing_artist_id_foreign');
|
||||
}
|
||||
|
||||
$table->dropColumn('contributing_artist_id');
|
||||
Schema::enableForeignKeyConstraints();
|
||||
if (DB::getDriverName() !== 'sqlite') { // @phpstan-ignore-line
|
||||
$table->dropForeign('songs_contributing_artist_id_foreign');
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
|
||||
$table->dropColumn('contributing_artist_id');
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,10 +5,6 @@ namespace Tests\Feature;
|
|||
use App\Models\User;
|
||||
use App\Services\LastfmService;
|
||||
use App\Services\TokenManager;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Log\Logger;
|
||||
use Laravel\Sanctum\NewAccessToken;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
use Mockery;
|
||||
|
@ -16,17 +12,6 @@ use Mockery\MockInterface;
|
|||
|
||||
class LastfmTest extends TestCase
|
||||
{
|
||||
public function testGetSessionKey(): void
|
||||
{
|
||||
/** @var Client $client */
|
||||
$client = Mockery::mock(Client::class, [
|
||||
'get' => new Response(200, [], file_get_contents(__DIR__ . '../../blobs/lastfm/session-key.json')),
|
||||
]);
|
||||
|
||||
$service = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
self::assertEquals('foo', $service->getSessionKey('bar'));
|
||||
}
|
||||
|
||||
public function testSetSessionKey(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Tests\Feature;
|
|||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\LastfmService;
|
||||
use Mockery;
|
||||
|
||||
class ScrobbleTest extends TestCase
|
||||
{
|
||||
|
@ -18,14 +19,16 @@ class ScrobbleTest extends TestCase
|
|||
/** @var Song $song */
|
||||
$song = Song::factory()->create();
|
||||
|
||||
$timestamp = time();
|
||||
|
||||
self::mock(LastfmService::class)
|
||||
->shouldReceive('scrobble')
|
||||
->with($song->album->artist->name, $song->title, $timestamp, $song->album->name, $user->lastfm_session_key)
|
||||
->with(
|
||||
Mockery::on(static fn (Song $s) => $s->is($song)),
|
||||
Mockery::on(static fn (User $u) => $u->is($user)),
|
||||
100
|
||||
)
|
||||
->once();
|
||||
|
||||
$this->postAs("/api/$song->id/scrobble", ['timestamp' => $timestamp], $user)
|
||||
$this->postAs("/api/$song->id/scrobble", ['timestamp' => 100], $user)
|
||||
->assertNoContent();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,62 +8,62 @@ use App\Models\Setting;
|
|||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\UploadService;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
class UploadTest extends TestCase
|
||||
{
|
||||
private UploadService|MockInterface $uploadService;
|
||||
private UploadedFile $file;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->uploadService = self::mock(UploadService::class);
|
||||
$this->file = UploadedFile::fake()
|
||||
->createWithContent('song.mp3', file_get_contents(__DIR__ . '/../songs/full.mp3'));
|
||||
}
|
||||
|
||||
public function testUnauthorizedPost(): void
|
||||
{
|
||||
Setting::set('media_path', '/media/koel');
|
||||
$file = UploadedFile::fake()->create('foo.mp3', 2048);
|
||||
|
||||
$this->uploadService
|
||||
->shouldReceive('handleUploadedFile')
|
||||
->never();
|
||||
|
||||
$this->postAs('/api/upload', ['file' => $file])->assertForbidden();
|
||||
$this->postAs('/api/upload', ['file' => $this->file])->assertForbidden();
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
public function provideUploadExceptions(): array
|
||||
{
|
||||
return [
|
||||
[MediaPathNotSetException::class, 403],
|
||||
[SongUploadFailedException::class, 400],
|
||||
[MediaPathNotSetException::class, Response::HTTP_FORBIDDEN],
|
||||
[SongUploadFailedException::class, Response::HTTP_BAD_REQUEST],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider provideUploadExceptions */
|
||||
public function testPostShouldFail(string $exceptionClass, int $statusCode): void
|
||||
{
|
||||
$file = UploadedFile::fake()->create('foo.mp3', 2048);
|
||||
|
||||
/** @var User $admin */
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->uploadService
|
||||
->shouldReceive('handleUploadedFile')
|
||||
->once()
|
||||
->with($file)
|
||||
->with($this->file)
|
||||
->andThrow($exceptionClass);
|
||||
|
||||
$this->postAs('/api/upload', ['file' => $file], $admin)->assertStatus($statusCode);
|
||||
$this->postAs('/api/upload', ['file' => $this->file], $admin)->assertStatus($statusCode);
|
||||
}
|
||||
|
||||
public function testPost(): void
|
||||
{
|
||||
Setting::set('media_path', '/media/koel');
|
||||
$file = UploadedFile::fake()->create('foo.mp3', 2048);
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->create();
|
||||
|
@ -74,9 +74,9 @@ class UploadTest extends TestCase
|
|||
$this->uploadService
|
||||
->shouldReceive('handleUploadedFile')
|
||||
->once()
|
||||
->with($file)
|
||||
->with($this->file)
|
||||
->andReturn($song);
|
||||
|
||||
$this->postAs('/api/upload', ['file' => $file], $admin)->assertJsonStructure(['song', 'album']);
|
||||
$this->postAs('/api/upload', ['file' => $this->file], $admin)->assertJsonStructure(['song', 'album']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ use GuzzleHttp\Handler\MockHandler;
|
|||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Log\Logger;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ApplicationInformationServiceTest extends TestCase
|
||||
|
@ -22,7 +21,7 @@ class ApplicationInformationServiceTest extends TestCase
|
|||
]);
|
||||
|
||||
$client = new Client(['handler' => HandlerStack::create($mock)]);
|
||||
$service = new ApplicationInformationService($client, app(Cache::class), app(Logger::class));
|
||||
$service = new ApplicationInformationService($client, app(Cache::class));
|
||||
|
||||
self::assertEquals($latestVersion, $service->getLatestVersionNumber());
|
||||
self::assertSame($latestVersion, cache()->get('latestKoelVersion'));
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\Services;
|
||||
|
||||
use App\Services\ITunesService;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Log\Logger;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ITunesServiceTest extends TestCase
|
||||
{
|
||||
public function testGetTrackUrl(): void
|
||||
{
|
||||
$term = 'Foo Bar';
|
||||
|
||||
/** @var Client $client */
|
||||
$client = Mockery::mock(Client::class, [
|
||||
'get' => new Response(200, [], file_get_contents(__DIR__ . '../../../blobs/itunes/track.json')),
|
||||
]);
|
||||
|
||||
$cache = app(Cache::class);
|
||||
$logger = app(Logger::class);
|
||||
|
||||
$url = (new ITunesService($client, $cache, $logger))->getTrackUrl($term);
|
||||
|
||||
self::assertEquals(
|
||||
'https://itunes.apple.com/us/album/i-remember-you/id265611220?i=265611396&uo=4&at=1000lsGu',
|
||||
$url
|
||||
);
|
||||
|
||||
self::assertNotNull(cache()->get('b57a14784d80c58a856e0df34ff0c8e2'));
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Services\LastfmService;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Log\Logger;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LastfmServiceTest extends TestCase
|
||||
{
|
||||
public function testGetArtistInformation(): void
|
||||
{
|
||||
/** @var Artist $artist */
|
||||
$artist = Artist::factory()->make(['name' => 'foo']);
|
||||
|
||||
/** @var Client $client */
|
||||
$client = Mockery::mock(Client::class, [
|
||||
'get' => new Response(200, [], file_get_contents(__DIR__ . '../../../blobs/lastfm/artist.json')),
|
||||
]);
|
||||
|
||||
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
$info = $api->getArtistInformation($artist);
|
||||
|
||||
self::assertEquals([
|
||||
'url' => 'https://www.last.fm/music/Kamelot',
|
||||
'image' => null,
|
||||
'bio' => [
|
||||
'summary' => 'Quisque ut nisi.',
|
||||
'full' => 'Quisque ut nisi. Vestibulum ullamcorper mauris at ligula.',
|
||||
],
|
||||
], $info->toArray());
|
||||
|
||||
self::assertNotNull(cache()->get('0aff3bc1259154f0e9db860026cda7a6'));
|
||||
}
|
||||
|
||||
public function testGetArtistInformationForNonExistentArtist(): void
|
||||
{
|
||||
/** @var Artist $artist */
|
||||
$artist = Artist::factory()->make();
|
||||
|
||||
/** @var Client $client */
|
||||
$client = Mockery::mock(Client::class, [
|
||||
'get' => new Response(400, [], file_get_contents(__DIR__ . '../../../blobs/lastfm/artist-notfound.json')),
|
||||
]);
|
||||
|
||||
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
|
||||
self::assertNull($api->getArtistInformation($artist));
|
||||
}
|
||||
|
||||
public function testGetAlbumInformation(): void
|
||||
{
|
||||
/** @var Artist $artist */
|
||||
$artist = Artist::factory()->create(['name' => 'bar']);
|
||||
|
||||
/** @var Album $album */
|
||||
$album = Album::factory()->for($artist)->create(['name' => 'foo']);
|
||||
|
||||
/** @var Client $client */
|
||||
$client = Mockery::mock(Client::class, [
|
||||
'get' => new Response(200, [], file_get_contents(__DIR__ . '../../../blobs/lastfm/album.json')),
|
||||
]);
|
||||
|
||||
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
$info = $api->getAlbumInformation($album);
|
||||
|
||||
self::assertEquals([
|
||||
'url' => 'https://www.last.fm/music/Kamelot/Epica',
|
||||
'cover' => null,
|
||||
'tracks' => [
|
||||
[
|
||||
'title' => 'Track 1',
|
||||
'url' => 'https://foo/track1',
|
||||
'length' => 100,
|
||||
],
|
||||
[
|
||||
'title' => 'Track 2',
|
||||
'url' => 'https://foo/track2',
|
||||
'length' => 150,
|
||||
],
|
||||
],
|
||||
'wiki' => [
|
||||
'summary' => 'Quisque ut nisi.',
|
||||
'full' => 'Quisque ut nisi. Vestibulum ullamcorper mauris at ligula.',
|
||||
],
|
||||
], $info->toArray());
|
||||
|
||||
self::assertNotNull(cache()->get('fca889d13b3222589d7d020669cc5a38'));
|
||||
}
|
||||
|
||||
public function testGetAlbumInformationForNonExistentAlbum(): void
|
||||
{
|
||||
/** @var Album $album */
|
||||
$album = Album::factory()->create();
|
||||
|
||||
/** @var Client $client */
|
||||
$client = Mockery::mock(Client::class, [
|
||||
'get' => new Response(400, [], file_get_contents(__DIR__ . '../../../blobs/lastfm/album-notfound.json')),
|
||||
]);
|
||||
|
||||
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
|
||||
|
||||
self::assertNull($api->getAlbumInformation($album));
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\Services;
|
||||
|
||||
use App\Services\YouTubeService;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Log\Logger;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class YouTubeServiceTest extends TestCase
|
||||
{
|
||||
public function testSearch(): void
|
||||
{
|
||||
$this->withoutEvents();
|
||||
|
||||
/** @var Client $client */
|
||||
$client = Mockery::mock(Client::class, [
|
||||
'get' => new Response(200, [], file_get_contents(__DIR__ . '../../../blobs/youtube/search.json')),
|
||||
]);
|
||||
|
||||
$api = new YouTubeService($client, app(Repository::class), app(Logger::class));
|
||||
$response = $api->search('Lorem Ipsum');
|
||||
|
||||
self::assertEquals('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title);
|
||||
self::assertNotNull(cache()->get('1492972ec5c8e6b3a9323ba719655ddb'));
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ class ScrobbleJobTest extends TestCase
|
|||
|
||||
$lastfm->shouldReceive('scrobble')
|
||||
->once()
|
||||
->with($song->artist->name, $song->title, 100, $song->album->name, $user->lastfm_session_key);
|
||||
->with($song, $user, 100);
|
||||
|
||||
$job->handle($lastfm);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ use App\Events\SongLikeToggled;
|
|||
use App\Listeners\LoveTrackOnLastfm;
|
||||
use App\Models\Interaction;
|
||||
use App\Services\LastfmService;
|
||||
use App\Values\LastfmLoveTrackParameters;
|
||||
use Mockery;
|
||||
use Tests\Feature\TestCase;
|
||||
|
||||
|
@ -18,17 +17,9 @@ class LoveTrackOnLastFmTest extends TestCase
|
|||
$interaction = Interaction::factory()->create();
|
||||
|
||||
$lastfm = Mockery::mock(LastfmService::class, ['enabled' => true]);
|
||||
$lastfm->shouldReceive('toggleLoveTrack')
|
||||
->with(
|
||||
Mockery::on(static function (LastfmLoveTrackParameters $params) use ($interaction): bool {
|
||||
self::assertSame($interaction->song->title, $params->trackName);
|
||||
self::assertSame($interaction->song->artist->name, $params->artistName);
|
||||
|
||||
return true;
|
||||
}),
|
||||
$interaction->user->lastfm_session_key,
|
||||
$interaction->liked
|
||||
);
|
||||
$lastfm->shouldReceive('toggleLoveTrack')
|
||||
->with($interaction->song, $interaction->user, $interaction->liked);
|
||||
|
||||
(new LoveTrackOnLastfm($lastfm))->handle(new SongLikeToggled($interaction));
|
||||
}
|
||||
|
|
|
@ -21,8 +21,9 @@ class UpdateLastfmNowPlayingTest extends TestCase
|
|||
$song = Song::factory()->create();
|
||||
|
||||
$lastfm = Mockery::mock(LastfmService::class, ['enabled' => true]);
|
||||
|
||||
$lastfm->shouldReceive('updateNowPlaying')
|
||||
->with($song->artist->name, $song->title, $song->album->name, $song->length, $user->lastfm_session_key);
|
||||
->with($song, $user);
|
||||
|
||||
(new UpdateLastfmNowPlaying($lastfm))->handle(new SongStartedPlaying($song, $user));
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
namespace Tests\Unit\Services\ApiClients;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||
use Illuminate\Log\Logger;
|
||||
use Mockery;
|
||||
use Mockery\LegacyMockInterface;
|
||||
use Mockery\MockInterface;
|
||||
|
@ -17,26 +15,22 @@ class ApiClientTest extends TestCase
|
|||
{
|
||||
use WithoutMiddleware;
|
||||
|
||||
private Cache|LegacyMockInterface|MockInterface $cache;
|
||||
private Client|LegacyMockInterface|MockInterface $client;
|
||||
private Logger|LegacyMockInterface|MockInterface $logger;
|
||||
private Client|LegacyMockInterface|MockInterface $wrapped;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->client = Mockery::mock(Client::class);
|
||||
$this->cache = Mockery::mock(Cache::class);
|
||||
$this->logger = Mockery::mock(Logger::class);
|
||||
$this->wrapped = Mockery::mock(Client::class);
|
||||
}
|
||||
|
||||
public function testBuildUri(): void
|
||||
{
|
||||
$api = new ConcreteApiClient($this->client, $this->cache, $this->logger);
|
||||
$api = new ConcreteApiClient($this->wrapped);
|
||||
|
||||
self::assertEquals('http://foo.com/get/param?key=bar', $api->buildUrl('get/param'));
|
||||
self::assertEquals('http://foo.com/get/param?baz=moo&key=bar', $api->buildUrl('/get/param?baz=moo'));
|
||||
self::assertEquals('http://baz.com/?key=bar', $api->buildUrl('http://baz.com/'));
|
||||
self::assertEquals('https://foo.com/get/param?key=bar', $api->buildUrl('get/param'));
|
||||
self::assertEquals('https://foo.com/get/param?baz=moo&key=bar', $api->buildUrl('/get/param?baz=moo'));
|
||||
self::assertEquals('https://baz.com/?key=bar', $api->buildUrl('https://baz.com/'));
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
|
@ -58,7 +52,7 @@ class ApiClientTest extends TestCase
|
|||
$method => new Response(200, [], $responseBody),
|
||||
]);
|
||||
|
||||
$api = new ConcreteApiClient($client, $this->cache, $this->logger);
|
||||
$api = new ConcreteApiClient($client);
|
||||
|
||||
self::assertSame((array) json_decode($responseBody), (array) $api->$method('/'));
|
||||
}
|
24
tests/Unit/Services/ApiClients/LastfmClientTest.php
Normal file
24
tests/Unit/Services/ApiClients/LastfmClientTest.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ApiClients;
|
||||
|
||||
use App\Services\ApiClients\LastfmClient;
|
||||
use GuzzleHttp\Client as GuzzleHttpClient;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LastfmClientTest extends TestCase
|
||||
{
|
||||
public function testGetSessionKey(): void
|
||||
{
|
||||
$mock = new MockHandler([
|
||||
new Response(200, [], file_get_contents(__DIR__ . '/../../../blobs/lastfm/session-key.json')),
|
||||
]);
|
||||
|
||||
$client = new LastfmClient(new GuzzleHttpClient(['handler' => HandlerStack::create($mock)]));
|
||||
|
||||
self::assertEquals('foo', $client->getSessionKey('bar'));
|
||||
}
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
namespace Tests\Unit\Services\ApiClients;
|
||||
|
||||
use App\Exceptions\SpotifyIntegrationDisabledException;
|
||||
use App\Services\SpotifyClient;
|
||||
use App\Services\ApiClients\SpotifyClient;
|
||||
use Illuminate\Cache\Repository as Cache;
|
||||
use Mockery;
|
||||
use Mockery\LegacyMockInterface;
|
||||
use Mockery\MockInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use SpotifyWebAPI\Session as SpotifySession;
|
||||
use SpotifyWebAPI\SpotifyWebAPI;
|
||||
use Tests\TestCase;
|
||||
|
@ -18,7 +17,6 @@ class SpotifyClientTest extends TestCase
|
|||
private SpotifySession|LegacyMockInterface|MockInterface $session;
|
||||
private SpotifyWebAPI|LegacyMockInterface|MockInterface $wrapped;
|
||||
private Cache|LegacyMockInterface|MockInterface $cache;
|
||||
private LoggerInterface|LegacyMockInterface|MockInterface $logger;
|
||||
|
||||
private SpotifyClient $client;
|
||||
|
||||
|
@ -34,14 +32,13 @@ class SpotifyClientTest extends TestCase
|
|||
$this->session = Mockery::mock(SpotifySession::class);
|
||||
$this->wrapped = Mockery::mock(SpotifyWebAPI::class);
|
||||
$this->cache = Mockery::mock(Cache::class);
|
||||
$this->logger = Mockery::mock(LoggerInterface::class);
|
||||
}
|
||||
|
||||
public function testAccessTokenIsSetUponInitialization(): void
|
||||
{
|
||||
$this->mockSetAccessToken();
|
||||
|
||||
$this->client = new SpotifyClient($this->wrapped, $this->session, $this->cache, $this->logger);
|
||||
$this->client = new SpotifyClient($this->wrapped, $this->session, $this->cache);
|
||||
}
|
||||
|
||||
public function testAccessTokenIsRetrievedFromCacheWhenApplicable(): void
|
||||
|
@ -53,7 +50,7 @@ class SpotifyClientTest extends TestCase
|
|||
$this->cache->shouldNotReceive('put');
|
||||
$this->wrapped->shouldReceive('setAccessToken')->with('fake-access-token');
|
||||
|
||||
$this->client = new SpotifyClient($this->wrapped, $this->session, $this->cache, $this->logger);
|
||||
$this->client = new SpotifyClient($this->wrapped, $this->session, $this->cache);
|
||||
}
|
||||
|
||||
public function testCallForwarding(): void
|
||||
|
@ -61,7 +58,7 @@ class SpotifyClientTest extends TestCase
|
|||
$this->mockSetAccessToken();
|
||||
$this->wrapped->shouldReceive('search')->with('foo', 'track')->andReturn('bar');
|
||||
|
||||
$this->client = new SpotifyClient($this->wrapped, $this->session, $this->cache, $this->logger);
|
||||
$this->client = new SpotifyClient($this->wrapped, $this->session, $this->cache);
|
||||
|
||||
self::assertSame('bar', $this->client->search('foo', 'track'));
|
||||
}
|
||||
|
@ -74,7 +71,7 @@ class SpotifyClientTest extends TestCase
|
|||
]);
|
||||
|
||||
self::expectException(SpotifyIntegrationDisabledException::class);
|
||||
(new SpotifyClient($this->wrapped, $this->session, $this->cache, $this->logger))->search('foo', 'track');
|
||||
(new SpotifyClient($this->wrapped, $this->session, $this->cache))->search('foo', 'track');
|
||||
}
|
||||
|
||||
private function mockSetAccessToken(): void
|
|
@ -2,13 +2,9 @@
|
|||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Services\ApiClients\ITunesClient;
|
||||
use App\Services\ITunesService;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Log\Logger;
|
||||
use Illuminate\Cache\Repository as Cache;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -33,6 +29,7 @@ class ITunesServiceTest extends TestCase
|
|||
'Foo',
|
||||
'Bar',
|
||||
'Baz',
|
||||
'Foo Bar Baz',
|
||||
'https://itunes.apple.com/bar',
|
||||
'https://itunes.apple.com/bar?at=foo',
|
||||
'2ce68c30758ed9496c72c36ff49c50b2',
|
||||
|
@ -40,6 +37,7 @@ class ITunesServiceTest extends TestCase
|
|||
'Foo',
|
||||
'',
|
||||
'Baz',
|
||||
'Foo Baz',
|
||||
'https://itunes.apple.com/bar?qux=qux',
|
||||
'https://itunes.apple.com/bar?qux=qux&at=foo',
|
||||
'cda57916eb80c2ee79b16e218bdb70d2',
|
||||
|
@ -52,24 +50,28 @@ class ITunesServiceTest extends TestCase
|
|||
string $term,
|
||||
string $album,
|
||||
string $artist,
|
||||
string $constructedTerm,
|
||||
string $trackViewUrl,
|
||||
string $affiliateUrl,
|
||||
string $cacheKey
|
||||
): void {
|
||||
config(['koel.itunes.affiliate_id' => 'foo']);
|
||||
|
||||
$mock = new MockHandler([
|
||||
new Response(200, [], json_encode([
|
||||
'resultCount' => 1,
|
||||
'results' => [['trackViewUrl' => $trackViewUrl]],
|
||||
])),
|
||||
]);
|
||||
|
||||
$client = new Client(['handler' => HandlerStack::create($mock)]);
|
||||
$cache = Mockery::mock(Cache::class);
|
||||
$logger = Mockery::mock(Logger::class);
|
||||
$client = Mockery::mock(ITunesClient::class);
|
||||
|
||||
$service = new ITunesService($client, $cache, $logger);
|
||||
$client->shouldReceive('get')
|
||||
->with('/', [
|
||||
'term' => $constructedTerm,
|
||||
'media' => 'music',
|
||||
'entity' => 'song',
|
||||
'limit' => 1,
|
||||
])
|
||||
->andReturn(json_decode(json_encode([
|
||||
'resultCount' => 1,
|
||||
'results' => [['trackViewUrl' => $trackViewUrl]],
|
||||
])));
|
||||
|
||||
$service = new ITunesService($client, $cache);
|
||||
|
||||
$cache
|
||||
->shouldReceive('remember')
|
||||
|
|
|
@ -2,41 +2,197 @@
|
|||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\ApiClients\LastfmClient;
|
||||
use App\Services\LastfmService;
|
||||
use Mockery;
|
||||
use Mockery\Mock;
|
||||
use Mockery\LegacyMockInterface;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LastfmServiceTest extends TestCase
|
||||
{
|
||||
public function testBuildAuthCallParams(): void
|
||||
private LastfmClient|MockInterface|LegacyMockInterface $client;
|
||||
private LastfmService $service;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
/** @var Mock|LastfmService $lastfm */
|
||||
$lastfm = Mockery::mock(LastfmService::class)->makePartial();
|
||||
$lastfm->shouldReceive('getKey')->andReturn('key');
|
||||
$lastfm->shouldReceive('getSecret')->andReturn('secret');
|
||||
parent::setUp();
|
||||
|
||||
$params = [
|
||||
'qux' => '安',
|
||||
'bar' => 'baz',
|
||||
];
|
||||
config([
|
||||
'koel.lastfm.key' => 'key',
|
||||
'koel.lastfm.secret' => 'secret',
|
||||
]);
|
||||
|
||||
// When I build Last.fm-compatible API parameters using the raw parameters
|
||||
$builtParams = $lastfm->buildAuthCallParams($params);
|
||||
$builtParamsAsString = $lastfm->buildAuthCallParams($params, true);
|
||||
$this->client = Mockery::mock(LastfmClient::class);
|
||||
$this->service = new LastfmService($this->client);
|
||||
}
|
||||
|
||||
public function testGetArtistInformation(): void
|
||||
{
|
||||
/** @var Artist $artist */
|
||||
$artist = Artist::factory()->make(['name' => 'foo']);
|
||||
|
||||
$this->client->shouldReceive('get')
|
||||
->with('?method=artist.getInfo&autocorrect=1&artist=foo&format=json')
|
||||
->once()
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/lastfm/artist.json')));
|
||||
|
||||
$info = $this->service->getArtistInformation($artist);
|
||||
|
||||
// Then I receive the Last.fm-compatible API parameters
|
||||
self::assertEquals([
|
||||
'api_key' => 'key',
|
||||
'bar' => 'baz',
|
||||
'qux' => '安',
|
||||
'api_sig' => '7f21233b54edea994aa0f23cf55f18a2',
|
||||
], $builtParams);
|
||||
'url' => 'https://www.last.fm/music/Kamelot',
|
||||
'image' => null,
|
||||
'bio' => [
|
||||
'summary' => 'Quisque ut nisi.',
|
||||
'full' => 'Quisque ut nisi. Vestibulum ullamcorper mauris at ligula.',
|
||||
],
|
||||
], $info->toArray());
|
||||
}
|
||||
|
||||
// And the string version as well
|
||||
self::assertEquals(
|
||||
'api_key=key&bar=baz&qux=安&api_sig=7f21233b54edea994aa0f23cf55f18a2',
|
||||
$builtParamsAsString
|
||||
);
|
||||
public function testGetArtistInformationForNonExistentArtist(): void
|
||||
{
|
||||
/** @var Artist $artist */
|
||||
$artist = Artist::factory()->make(['name' => 'bar']);
|
||||
|
||||
$this->client->shouldReceive('get')
|
||||
->with('?method=artist.getInfo&autocorrect=1&artist=bar&format=json')
|
||||
->once()
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/lastfm/artist-notfound.json')));
|
||||
|
||||
self::assertNull($this->service->getArtistInformation($artist));
|
||||
}
|
||||
|
||||
public function testGetAlbumInformation(): void
|
||||
{
|
||||
/** @var Album $album */
|
||||
$album = Album::factory()->for(Artist::factory()->create(['name' => 'bar']))->create(['name' => 'foo']);
|
||||
|
||||
$this->client->shouldReceive('get')
|
||||
->with('?method=album.getInfo&autocorrect=1&album=foo&artist=bar&format=json')
|
||||
->once()
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/lastfm/album.json')));
|
||||
|
||||
$info = $this->service->getAlbumInformation($album);
|
||||
|
||||
self::assertEquals([
|
||||
'url' => 'https://www.last.fm/music/Kamelot/Epica',
|
||||
'cover' => null,
|
||||
'tracks' => [
|
||||
[
|
||||
'title' => 'Track 1',
|
||||
'url' => 'https://foo/track1',
|
||||
'length' => 100,
|
||||
],
|
||||
[
|
||||
'title' => 'Track 2',
|
||||
'url' => 'https://foo/track2',
|
||||
'length' => 150,
|
||||
],
|
||||
],
|
||||
'wiki' => [
|
||||
'summary' => 'Quisque ut nisi.',
|
||||
'full' => 'Quisque ut nisi. Vestibulum ullamcorper mauris at ligula.',
|
||||
],
|
||||
], $info->toArray());
|
||||
}
|
||||
|
||||
public function testGetAlbumInformationForNonExistentAlbum(): void
|
||||
{
|
||||
/** @var Album $album */
|
||||
$album = Album::factory()->for(Artist::factory()->create(['name' => 'bar']))->create(['name' => 'foo']);
|
||||
|
||||
$this->client->shouldReceive('get')
|
||||
->with('?method=album.getInfo&autocorrect=1&album=foo&artist=bar&format=json')
|
||||
->once()
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/lastfm/album-notfound.json')));
|
||||
|
||||
self::assertNull($this->service->getAlbumInformation($album));
|
||||
}
|
||||
|
||||
public function testScrobble(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create([
|
||||
'preferences' => [
|
||||
'lastfm_session_key' => 'my_key',
|
||||
],
|
||||
]);
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->create();
|
||||
|
||||
$this->service->scrobble($song, $user, 100);
|
||||
|
||||
$this->client->shouldHaveReceived('post')
|
||||
->with('/', [
|
||||
'artist' => $song->artist->name,
|
||||
'track' => $song->title,
|
||||
'timestamp' => 100,
|
||||
'sk' => 'my_key',
|
||||
'method' => 'track.scrobble',
|
||||
'album' => $song->album->name,
|
||||
], false)
|
||||
->once();
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
public function provideToggleLoveTrackData(): array
|
||||
{
|
||||
return [[true, 'track.love'], [false, 'track.unlove']];
|
||||
}
|
||||
|
||||
/** @dataProvider provideToggleLoveTrackData */
|
||||
public function testToggleLoveTrack(bool $love, string $method): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create([
|
||||
'preferences' => [
|
||||
'lastfm_session_key' => 'my_key',
|
||||
],
|
||||
]);
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->for(Artist::factory()->create(['name' => 'foo']))->create(['title' => 'bar']);
|
||||
|
||||
$this->service->toggleLoveTrack($song, $user, $love);
|
||||
|
||||
$this->client->shouldHaveReceived('post')
|
||||
->with('/', [
|
||||
'artist' => 'foo',
|
||||
'track' => 'bar',
|
||||
'sk' => 'my_key',
|
||||
'method' => $method,
|
||||
], false)
|
||||
->once();
|
||||
}
|
||||
|
||||
public function testUpdateNowPlaying(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create([
|
||||
'preferences' => [
|
||||
'lastfm_session_key' => 'my_key',
|
||||
],
|
||||
]);
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->for(Artist::factory()->create(['name' => 'foo']))->create(['title' => 'bar']);
|
||||
|
||||
$this->service->updateNowPlaying($song, $user);
|
||||
|
||||
$this->client->shouldHaveReceived('post')
|
||||
->with('/', [
|
||||
'artist' => 'foo',
|
||||
'track' => 'bar',
|
||||
'duration' => $song->length,
|
||||
'sk' => 'my_key',
|
||||
'method' => 'track.updateNowPlaying',
|
||||
'album' => $song->album->name,
|
||||
], false)
|
||||
->once();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\Services;
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Services\LastfmService;
|
||||
use App\Services\MediaInformationService;
|
||||
use App\Services\MediaMetadataService;
|
||||
use App\Services\MusicEncyclopedia;
|
||||
use App\Values\AlbumInformation;
|
||||
use App\Values\ArtistInformation;
|
||||
use Illuminate\Cache\Repository as Cache;
|
||||
use Mockery;
|
||||
use Mockery\LegacyMockInterface;
|
||||
use Mockery\MockInterface;
|
||||
|
@ -16,7 +18,7 @@ use Tests\TestCase;
|
|||
|
||||
class MediaInformationServiceTest extends TestCase
|
||||
{
|
||||
private LastfmService|MockInterface|LegacyMockInterface $lastFmService;
|
||||
private MusicEncyclopedia|MockInterface|LegacyMockInterface $encyclopedia;
|
||||
private MediaMetadataService|LegacyMockInterface|MockInterface $mediaMetadataService;
|
||||
private MediaInformationService $mediaInformationService;
|
||||
|
||||
|
@ -24,10 +26,14 @@ class MediaInformationServiceTest extends TestCase
|
|||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->lastFmService = Mockery::mock(LastfmService::class);
|
||||
$this->encyclopedia = Mockery::mock(LastfmService::class);
|
||||
$this->mediaMetadataService = Mockery::mock(MediaMetadataService::class);
|
||||
|
||||
$this->mediaInformationService = new MediaInformationService($this->lastFmService, $this->mediaMetadataService);
|
||||
$this->mediaInformationService = new MediaInformationService(
|
||||
$this->encyclopedia,
|
||||
$this->mediaMetadataService,
|
||||
app(Cache::class)
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetAlbumInformation(): void
|
||||
|
@ -36,13 +42,14 @@ class MediaInformationServiceTest extends TestCase
|
|||
$album = Album::factory()->create();
|
||||
$info = AlbumInformation::make();
|
||||
|
||||
$this->lastFmService
|
||||
$this->encyclopedia
|
||||
->shouldReceive('getAlbumInformation')
|
||||
->once()
|
||||
->with($album)
|
||||
->andReturn($info);
|
||||
|
||||
self::assertSame($info, $this->mediaInformationService->getAlbumInformation($album));
|
||||
self::assertNotNull(cache()->get('album.info.' . $album->id));
|
||||
}
|
||||
|
||||
public function testGetAlbumInformationTriesDownloadingCover(): void
|
||||
|
@ -51,7 +58,7 @@ class MediaInformationServiceTest extends TestCase
|
|||
$album = Album::factory()->create(['cover' => '']);
|
||||
$info = AlbumInformation::make();
|
||||
|
||||
$this->lastFmService
|
||||
$this->encyclopedia
|
||||
->shouldReceive('getAlbumInformation')
|
||||
->once()
|
||||
->with($album)
|
||||
|
@ -70,13 +77,14 @@ class MediaInformationServiceTest extends TestCase
|
|||
$artist = Artist::factory()->create();
|
||||
$info = ArtistInformation::make();
|
||||
|
||||
$this->lastFmService
|
||||
$this->encyclopedia
|
||||
->shouldReceive('getArtistInformation')
|
||||
->once()
|
||||
->with($artist)
|
||||
->andReturn($info);
|
||||
|
||||
self::assertSame($info, $this->mediaInformationService->getArtistInformation($artist));
|
||||
self::assertNotNull(cache()->get('artist.info.' . $artist->id));
|
||||
}
|
||||
|
||||
public function testGetArtistInformationTriesDownloadingImage(): void
|
||||
|
@ -85,7 +93,7 @@ class MediaInformationServiceTest extends TestCase
|
|||
$artist = Artist::factory()->create(['image' => '']);
|
||||
$info = ArtistInformation::make();
|
||||
|
||||
$this->lastFmService
|
||||
$this->encyclopedia
|
||||
->shouldReceive('getArtistInformation')
|
||||
->once()
|
||||
->with($artist)
|
|
@ -10,7 +10,6 @@ use App\Services\SpotifyService;
|
|||
use Mockery;
|
||||
use Mockery\LegacyMockInterface;
|
||||
use Mockery\MockInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MediaMetadataServiceTest extends TestCase
|
||||
|
@ -26,11 +25,7 @@ class MediaMetadataServiceTest extends TestCase
|
|||
$this->spotifyService = Mockery::mock(SpotifyService::class);
|
||||
$this->imageWriter = Mockery::mock(ImageWriter::class);
|
||||
|
||||
$this->mediaMetadataService = new MediaMetadataService(
|
||||
$this->spotifyService,
|
||||
$this->imageWriter,
|
||||
app(LoggerInterface::class)
|
||||
);
|
||||
$this->mediaMetadataService = new MediaMetadataService($this->spotifyService, $this->imageWriter);
|
||||
}
|
||||
|
||||
public function testTryDownloadAlbumCover(): void
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace Tests\Unit\Services;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Services\SpotifyClient;
|
||||
use App\Services\ApiClients\SpotifyClient;
|
||||
use App\Services\SpotifyService;
|
||||
use Mockery;
|
||||
use Mockery\LegacyMockInterface;
|
||||
|
|
31
tests/Unit/Services/YouTubeServiceTest.php
Normal file
31
tests/Unit/Services/YouTubeServiceTest.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use App\Services\ApiClients\YouTubeClient;
|
||||
use App\Services\YouTubeService;
|
||||
use Illuminate\Cache\Repository;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class YouTubeServiceTest extends TestCase
|
||||
{
|
||||
public function testSearchVideosRelatedToSong(): void
|
||||
{
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->for(Artist::factory()->create(['name' => 'Bar']))->create(['title' => 'Foo']);
|
||||
$client = Mockery::mock(YouTubeClient::class);
|
||||
|
||||
$client->shouldReceive('get')
|
||||
->with('search?part=snippet&type=video&maxResults=10&pageToken=my-token&q=Foo+Bar')
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/youtube/search.json')));
|
||||
|
||||
$service = new YouTubeService($client, app(Repository::class));
|
||||
$response = $service->searchVideosRelatedToSong($song, 'my-token');
|
||||
|
||||
self::assertEquals('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title);
|
||||
self::assertNotNull(cache()->get('5becf539115b18b2df11c39adbc2bdfa'));
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Tests\Unit\Stubs;
|
||||
|
||||
use App\Services\ApiClient;
|
||||
use App\Services\ApiClients\ApiClient;
|
||||
|
||||
class ConcreteApiClient extends ApiClient
|
||||
{
|
||||
|
@ -18,6 +18,6 @@ class ConcreteApiClient extends ApiClient
|
|||
|
||||
public function getEndpoint(): string
|
||||
{
|
||||
return 'http://foo.com';
|
||||
return 'https://foo.com';
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue