feat(lastfm): batch like/unlike are now asynchronous

This commit is contained in:
Phan An 2021-06-04 17:19:33 +02:00
parent 6c24b529cc
commit ef1add3877
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
28 changed files with 358 additions and 359 deletions

View file

@ -0,0 +1,21 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
class SongsBatchLiked extends Event
{
use SerializesModels;
public $songs;
public $user;
public function __construct(Collection $songs, User $user)
{
$this->songs = $songs;
$this->user = $user;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
class SongsBatchUnliked
{
use SerializesModels;
public $songs;
public $user;
public function __construct(Collection $songs, User $user)
{
$this->songs = $songs;
$this->user = $user;
}
}

View file

@ -26,7 +26,7 @@ class ScrobbleController extends Controller
public function store(ScrobbleStoreRequest $request, Song $song) public function store(ScrobbleStoreRequest $request, Song $song)
{ {
if (!$song->artist->is_unknown && $this->currentUser->connectedToLastfm()) { if (!$song->artist->is_unknown && $this->currentUser->connectedToLastfm()) {
ScrobbleJob::dispatch($this->currentUser, $song, (int) $request->timestamp); ScrobbleJob::dispatch($this->currentUser, $song, $request->timestamp);
} }
return response()->json(null, Response::HTTP_NO_CONTENT); return response()->json(null, Response::HTTP_NO_CONTENT);

View file

@ -1,20 +0,0 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
abstract class Job
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "onQueue" and "delay" queue helper methods.
|
*/
use Queueable;
}

View file

@ -1,39 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Interaction;
use App\Models\User;
use App\Services\LastfmService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class LoveTrackOnLastfmJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
private $user;
private $interaction;
public function __construct(User $user, Interaction $interaction)
{
$this->user = $user;
$this->interaction = $interaction;
}
public function handle(LastfmService $lastfmService): void
{
$lastfmService->toggleLoveTrack(
$this->interaction->song->title,
$this->interaction->song->artist->name,
$this->user->lastfm_session_key,
$this->interaction->liked
);
}
}

View file

@ -1,41 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Album;
use App\Models\Song;
use App\Models\User;
use App\Services\LastfmService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class UpdateLastfmNowPlayingJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
private $user;
private $song;
public function __construct(User $user, Song $song)
{
$this->user = $user;
$this->song = $song;
}
public function handle(LastfmService $lastfmService): void
{
$lastfmService->updateNowPlaying(
$this->song->artist->name,
$this->song->title,
$this->song->album->name === Album::UNKNOWN_NAME ? '' : $this->song->album->name,
$this->song->length,
$this->user->lastfm_session_key
);
}
}

View file

View file

@ -0,0 +1,29 @@
<?php
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 $lastfm;
public function __construct(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
);
}
}

View file

@ -3,10 +3,11 @@
namespace App\Listeners; namespace App\Listeners;
use App\Events\SongLikeToggled; use App\Events\SongLikeToggled;
use App\Jobs\LoveTrackOnLastfmJob;
use App\Services\LastfmService; use App\Services\LastfmService;
use App\Values\LastfmLoveTrackParameters;
use Illuminate\Contracts\Queue\ShouldQueue;
class LoveTrackOnLastfm class LoveTrackOnLastfm implements ShouldQueue
{ {
private $lastfm; private $lastfm;
@ -25,6 +26,13 @@ class LoveTrackOnLastfm
return; return;
} }
LoveTrackOnLastfmJob::dispatch($event->user, $event->interaction); $this->lastfm->toggleLoveTrack(
LastfmLoveTrackParameters::make(
$event->interaction->song->title,
$event->interaction->song->artist->name,
),
$event->user->lastfm_session_key,
$event->interaction->liked
);
} }
} }

View file

@ -0,0 +1,22 @@
<?php
namespace App\Listeners;
use App\Events\SongsBatchUnliked;
use App\Services\LastfmService;
use Illuminate\Contracts\Queue\ShouldQueue;
class UnloveMultipleTracksOnLastfm implements ShouldQueue
{
private $lastfm;
public function __construct(LastfmService $lastfm)
{
$this->lastfm = $lastfm;
}
public function handle(SongsBatchUnliked $event): void
{
$this->lastfm->batchToggleLoveTracks($event->songs, $event->user, false);
}
}

View file

@ -3,10 +3,10 @@
namespace App\Listeners; namespace App\Listeners;
use App\Events\SongStartedPlaying; use App\Events\SongStartedPlaying;
use App\Jobs\UpdateLastfmNowPlayingJob;
use App\Services\LastfmService; use App\Services\LastfmService;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateLastfmNowPlaying class UpdateLastfmNowPlaying implements ShouldQueue
{ {
private $lastfm; private $lastfm;
@ -21,6 +21,12 @@ class UpdateLastfmNowPlaying
return; return;
} }
UpdateLastfmNowPlayingJob::dispatch($event->user, $event->song); $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
);
} }
} }

View file

@ -15,9 +15,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property User $user * @property User $user
* @property int $id * @property int $id
* *
* @method self firstOrCreate(array $where, array $params = []) * @method static self firstOrCreate(array $where, array $params = [])
* @method static self find(int $id) * @method static self find(int $id)
* @method static Builder whereSongIdAndUserId(string $songId, string $userId) * @method static Builder whereSongIdAndUserId(string $songId, string $userId)
* @method static Builder whereIn(...$params)
*/ */
class Interaction extends Model class Interaction extends Model
{ {

View file

@ -36,7 +36,7 @@ use Laravel\Scout\Searchable;
* @method static self first() * @method static self first()
* @method static EloquentCollection orderBy(...$args) * @method static EloquentCollection orderBy(...$args)
* @method static int count() * @method static int count()
* @method static self|null find($id) * @method static self|Collection|null find($id)
* @method static Builder take(int $count) * @method static Builder take(int $count)
*/ */
class Song extends Model class Song extends Model

View file

@ -7,12 +7,16 @@ use App\Events\ArtistInformationFetched;
use App\Events\LibraryChanged; use App\Events\LibraryChanged;
use App\Events\MediaCacheObsolete; use App\Events\MediaCacheObsolete;
use App\Events\SongLikeToggled; use App\Events\SongLikeToggled;
use App\Events\SongsBatchLiked;
use App\Events\SongsBatchUnliked;
use App\Events\SongStartedPlaying; use App\Events\SongStartedPlaying;
use App\Listeners\ClearMediaCache; use App\Listeners\ClearMediaCache;
use App\Listeners\DownloadAlbumCover; use App\Listeners\DownloadAlbumCover;
use App\Listeners\DownloadArtistImage; use App\Listeners\DownloadArtistImage;
use App\Listeners\LoveMultipleTracksOnLastfm;
use App\Listeners\LoveTrackOnLastfm; use App\Listeners\LoveTrackOnLastfm;
use App\Listeners\TidyLibrary; use App\Listeners\TidyLibrary;
use App\Listeners\UnloveMultipleTracksOnLastfm;
use App\Listeners\UpdateLastfmNowPlaying; use App\Listeners\UpdateLastfmNowPlaying;
use App\Models\Album; use App\Models\Album;
use App\Models\Song; use App\Models\Song;
@ -32,6 +36,14 @@ class EventServiceProvider extends ServiceProvider
LoveTrackOnLastfm::class, LoveTrackOnLastfm::class,
], ],
SongsBatchLiked::class => [
LoveMultipleTracksOnLastfm::class,
],
SongsBatchUnliked::class => [
UnloveMultipleTracksOnLastfm::class,
],
SongStartedPlaying::class => [ SongStartedPlaying::class => [
UpdateLastfmNowPlaying::class, UpdateLastfmNowPlaying::class,
], ],

View file

@ -4,21 +4,45 @@ namespace App\Services;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Promise\Promise;
use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Log\Logger; use Illuminate\Log\Logger;
use Illuminate\Support\Str;
use InvalidArgumentException; use InvalidArgumentException;
use SimpleXMLElement; use SimpleXMLElement;
use Webmozart\Assert\Assert;
/** /**
* @method object get($uri, ...$args) * @method object get(string $uri, array $data = [], bool $appendKey = true)
* @method object post($uri, ...$data) * @method object post($uri, array $data = [], bool $appendKey = true)
* @method object put($uri, ...$data) * @method object put($uri, array $data = [], bool $appendKey = true)
* @method object patch($uri, ...$data) * @method object patch($uri, array $data = [], bool $appendKey = true)
* @method object head($uri, ...$data) * @method object head($uri, array $data = [], bool $appendKey = true)
* @method object delete($uri) * @method object 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)
* @method Promise patchAsync($uri, array $data = [], bool $appendKey = true)
* @method Promise headAsync($uri, array $data = [], bool $appendKey = true)
* @method Promise deleteAsync($uri, array $data = [], bool $appendKey = true)
*/ */
abstract class AbstractApiClient abstract class AbstractApiClient
{ {
private const MAGIC_METHODS = [
'get',
'post',
'put',
'patch',
'head',
'delete',
'getAsync',
'postAsync',
'putAsync',
'patchAsync',
'headAsync',
'deleteAsync',
];
protected $responseFormat = 'json'; protected $responseFormat = 'json';
protected $client; protected $client;
protected $cache; protected $cache;
@ -50,7 +74,7 @@ abstract class AbstractApiClient
* an "API signature" of the request. Appending an API key will break the request. * an "API signature" of the request. Appending an API key will break the request.
* @param array<mixed> $params An array of parameters * @param array<mixed> $params An array of parameters
* *
* @return mixed|SimpleXMLElement|null * @return mixed|SimpleXMLElement|void
*/ */
public function request(string $method, string $uri, bool $appendKey = true, array $params = []) public function request(string $method, string $uri, bool $appendKey = true, array $params = [])
{ {
@ -73,6 +97,11 @@ abstract class AbstractApiClient
} }
} }
public function requestAsync(string $method, string $uri, bool $appendKey = true, array $params = []): Promise
{
return $this->getClient()->$method($this->buildUrl($uri, $appendKey), ['form_params' => $params]);
}
/** /**
* Make an HTTP call to the external resource. * Make an HTTP call to the external resource.
* *
@ -81,19 +110,25 @@ abstract class AbstractApiClient
* *
* @throws InvalidArgumentException * @throws InvalidArgumentException
* *
* @return mixed|SimpleXMLElement|null * @return mixed|SimpleXMLElement|void
*/ */
public function __call(string $method, array $args) public function __call(string $method, array $args)
{ {
Assert::inArray($method, self::MAGIC_METHODS);
if (count($args) < 1) { if (count($args) < 1) {
throw new InvalidArgumentException('Magic request methods require a URI and optional options array'); throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
} }
$uri = $args[0]; $uri = $args[0];
$opts = $args[1] ?? []; $params = $args[1] ?? [];
$appendKey = $args[2] ?? true; $appendKey = $args[2] ?? true;
return $this->request($method, $uri, $appendKey, $opts); if (Str::endsWith($method, 'Async')) {
return $this->requestAsync($method, $uri, $appendKey, $params);
} else {
return $this->request($method, $uri, $appendKey, $params);
}
} }
/** /**
@ -113,9 +148,9 @@ abstract class AbstractApiClient
if ($appendKey) { if ($appendKey) {
if (parse_url($uri, PHP_URL_QUERY)) { if (parse_url($uri, PHP_URL_QUERY)) {
$uri .= "&{$this->keyParam}=" . $this->getKey(); $uri .= "&$this->keyParam=" . $this->getKey();
} else { } else {
$uri .= "?{$this->keyParam}=" . $this->getKey(); $uri .= "?$this->keyParam=" . $this->getKey();
} }
} }

View file

@ -3,18 +3,15 @@
namespace App\Services; namespace App\Services;
use App\Events\SongLikeToggled; use App\Events\SongLikeToggled;
use App\Events\SongsBatchLiked;
use App\Events\SongsBatchUnliked;
use App\Models\Interaction; use App\Models\Interaction;
use App\Models\Song;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Collection;
class InteractionService class InteractionService
{ {
private $interaction;
public function __construct(Interaction $interaction)
{
$this->interaction = $interaction;
}
/** /**
* Increase the number of times a song is played by a user. * Increase the number of times a song is played by a user.
* *
@ -22,7 +19,7 @@ class InteractionService
*/ */
public function increasePlayCount(string $songId, User $user): Interaction public function increasePlayCount(string $songId, User $user): Interaction
{ {
return tap($this->interaction->firstOrCreate([ return tap(Interaction::firstOrCreate([
'song_id' => $songId, 'song_id' => $songId,
'user_id' => $user->id, 'user_id' => $user->id,
]), static function (Interaction $interaction): void { ]), static function (Interaction $interaction): void {
@ -42,7 +39,7 @@ class InteractionService
*/ */
public function toggleLike(string $songId, User $user): Interaction public function toggleLike(string $songId, User $user): Interaction
{ {
return tap($this->interaction->firstOrCreate([ return tap(Interaction::firstOrCreate([
'song_id' => $songId, 'song_id' => $songId,
'user_id' => $user->id, 'user_id' => $user->id,
]), static function (Interaction $interaction): void { ]), static function (Interaction $interaction): void {
@ -60,10 +57,10 @@ class InteractionService
* *
* @return array<Interaction> the array of Interaction objects * @return array<Interaction> the array of Interaction objects
*/ */
public function batchLike(array $songIds, User $user): array public function batchLike(array $songIds, User $user): Collection
{ {
return collect($songIds)->map(function ($songId) use ($user): Interaction { $interactions = collect($songIds)->map(static function ($songId) use ($user): Interaction {
return tap($this->interaction->firstOrCreate([ return tap(Interaction::firstOrCreate([
'song_id' => $songId, 'song_id' => $songId,
'user_id' => $user->id, 'user_id' => $user->id,
]), static function (Interaction $interaction): void { ]), static function (Interaction $interaction): void {
@ -73,10 +70,14 @@ class InteractionService
$interaction->liked = true; $interaction->liked = true;
$interaction->save(); $interaction->save();
event(new SongLikeToggled($interaction));
}); });
})->all(); });
event(new SongsBatchLiked($interactions->map(static function (Interaction $interaction): Song {
return $interaction->song;
}), $user));
return $interactions;
} }
/** /**
@ -86,17 +87,10 @@ class InteractionService
*/ */
public function batchUnlike(array $songIds, User $user): void public function batchUnlike(array $songIds, User $user): void
{ {
$this->interaction Interaction::whereIn('song_id', $songIds)
->whereIn('song_id', $songIds)
->where('user_id', $user->id) ->where('user_id', $user->id)
->get() ->update(['liked' => false]);
->each(
static function (Interaction $interaction): void {
$interaction->liked = false;
$interaction->save();
event(new SongLikeToggled($interaction)); event(new SongsBatchUnliked(Song::find($songIds), $user));
}
);
} }
} }

View file

@ -2,12 +2,16 @@
namespace App\Services; namespace App\Services;
use App\Values\LastfmLoveTrackParameters;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\Utils;
use Illuminate\Support\Collection;
use Throwable; use Throwable;
class LastfmService extends AbstractApiClient implements ApiConsumerInterface class LastfmService extends AbstractApiClient implements ApiConsumerInterface
{ {
/** /**
* Override the key param, since, again, Lastfm wants to be different. * Override the key param, since, again, Last.fm wants to be different.
*/ */
protected $keyParam = 'api_key'; protected $keyParam = 'api_key';
@ -27,11 +31,7 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
return $this->getKey() && $this->getSecret(); return $this->getKey() && $this->getSecret();
} }
/** /** @return array<mixed>|null */
* Get information about an artist.
*
* @return array<mixed>|null
*/
public function getArtistInformation(string $name): ?array public function getArtistInformation(string $name): ?array
{ {
if (!$this->enabled()) { if (!$this->enabled()) {
@ -76,11 +76,7 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
]; ];
} }
/** /** @return array<mixed>|null */
* Get information about an album.
*
* @return array<mixed>|null
*/
public function getAlbumInformation(string $albumName, string $artistName): ?array public function getAlbumInformation(string $albumName, string $artistName): ?array
{ {
if (!$this->enabled()) { if (!$this->enabled()) {
@ -159,25 +155,25 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
} }
} }
/** public function scrobble(
* Scrobble a song. string $artistName,
* string $trackName,
* @param string $artist The artist name $timestamp,
* @param string $track The track name string $albumName,
* @param string|int $timestamp The UNIX timestamp string $sessionKey
* @param string $album The album name ): void {
* @param string $sk The session key $params = [
*/ 'artist' => $artistName,
public function scrobble(string $artist, string $track, $timestamp, string $album, string $sk): void 'track' => $trackName,
{ 'timestamp' => $timestamp,
$params = compact('artist', 'track', 'timestamp', 'sk'); 'sk' => $sessionKey,
'method' => 'track.scrobble',
];
if ($album) { if ($albumName) {
$params['album'] = $album; $params['album'] = $albumName;
} }
$params['method'] = 'track.scrobble';
try { try {
$this->post('/', $this->buildAuthCallParams($params), false); $this->post('/', $this->buildAuthCallParams($params), false);
} catch (Throwable $e) { } catch (Throwable $e) {
@ -185,42 +181,63 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
} }
} }
/** public function toggleLoveTrack(LastfmLoveTrackParameters $params, string $sessionKey, bool $love = true): void
* Love or unlove a track on Last.fm.
*
* @param string $track The track name
* @param string $artist The artist's name
* @param string $sk The session key
* @param bool $love Whether to love or unlove. Such cheesy terms... urrgggh
*/
public function toggleLoveTrack(string $track, string $artist, string $sk, ?bool $love = true): void
{ {
$params = compact('track', 'artist', 'sk');
$params['method'] = $love ? 'track.love' : 'track.unlove';
try { try {
$this->post('/', $this->buildAuthCallParams($params), false); $this->post('/', $this->buildAuthCallParams([
'track' => $params->getTrackName(),
'artist' => $params->getArtistName(),
'sk' => $sessionKey,
'method' => $love ? 'track.love' : 'track.unlove',
]), false);
} catch (Throwable $e) {
$this->logger->error($e);
}
}
/**
* @param Collection|array<LastfmLoveTrackParameters> $parameterCollection
*/
public function batchToggleLoveTracks(Collection $parameterCollection, string $sessionKey, bool $love = true): void
{
$promises = $parameterCollection->map(
function (LastfmLoveTrackParameters $params) use ($sessionKey, $love): Promise {
return $this->postAsync('/', $this->buildAuthCallParams([
'track' => $params->getTrackName(),
'artist' => $params->getArtistName(),
'sk' => $sessionKey,
'method' => $love ? 'track.love' : 'track.unlove',
]), false);
}
);
try {
Utils::unwrap($promises);
} catch (Throwable $e) { } catch (Throwable $e) {
$this->logger->error($e); $this->logger->error($e);
} }
} }
/** /**
* Update a track's "now playing" on Last.fm.
*
* @param string $artist Name of the artist
* @param string $track Name of the track
* @param string $album Name of the album
* @param int|float $duration Duration of the track, in seconds * @param int|float $duration Duration of the track, in seconds
* @param string $sk The session key
*/ */
public function updateNowPlaying(string $artist, string $track, string $album, $duration, string $sk): void public function updateNowPlaying(
{ string $artistName,
$params = compact('artist', 'track', 'duration', 'sk'); string $trackName,
$params['method'] = 'track.updateNowPlaying'; string $albumName,
$duration,
string $sessionKey
): void {
$params = [
'artist' => $artistName,
'track' => $trackName,
'duration' => $duration,
'sk' => $sessionKey,
'method' => 'track.updateNowPlaying',
];
if ($album) { if ($albumName) {
$params['album'] = $album; $params['album'] = $albumName;
} }
try { try {

View file

@ -0,0 +1,30 @@
<?php
namespace App\Values;
class LastfmLoveTrackParameters
{
private $trackName;
private $artistName;
private function __construct(string $trackName, string $artistName)
{
$this->trackName = $trackName;
$this->artistName = $artistName;
}
public static function make(string $trackName, string $artistName): self
{
return new static($trackName, $artistName);
}
public function getTrackName(): string
{
return $this->trackName;
}
public function getArtistName(): string
{
return $this->artistName;
}
}

View file

@ -29,7 +29,8 @@
"lstrojny/functional-php": "^1.14", "lstrojny/functional-php": "^1.14",
"teamtnt/laravel-scout-tntsearch-driver": "^11.1", "teamtnt/laravel-scout-tntsearch-driver": "^11.1",
"algolia/algoliasearch-client-php": "^2.7", "algolia/algoliasearch-client-php": "^2.7",
"laravel/ui": "^3.2" "laravel/ui": "^3.2",
"webmozart/assert": "^1.10"
}, },
"require-dev": { "require-dev": {
"facade/ignition": "^2.5", "facade/ignition": "^2.5",

2
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "5f54c3d8584004c6454de204fd4c4509", "content-hash": "058ece2df4d81093f9268a416cb1d0ef",
"packages": [ "packages": [
{ {
"name": "algolia/algoliasearch-client-php", "name": "algolia/algoliasearch-client-php",

View file

@ -5,6 +5,7 @@ namespace Database\Factories;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class UserFactory extends Factory class UserFactory extends Factory
{ {
@ -18,8 +19,10 @@ class UserFactory extends Factory
'email' => $this->faker->email, 'email' => $this->faker->email,
'password' => Hash::make('secret'), 'password' => Hash::make('secret'),
'is_admin' => false, 'is_admin' => false,
'preferences' => [], 'preferences' => [
'remember_token' => str_random(10), 'lastfm_session_key' => Str::random(),
],
'remember_token' => Str::random(10),
]; ];
} }

View file

@ -3,8 +3,10 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Events\SongLikeToggled; use App\Events\SongLikeToggled;
use App\Events\SongsBatchLiked;
use App\Models\Song; use App\Models\Song;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Collection;
class InteractionTest extends TestCase class InteractionTest extends TestCase
{ {
@ -66,10 +68,12 @@ class InteractionTest extends TestCase
public function testToggleBatch(): void public function testToggleBatch(): void
{ {
$this->expectsEvents(SongLikeToggled::class); $this->expectsEvents(SongsBatchLiked::class);
/** @var User $user */
$user = User::factory()->create(); $user = User::factory()->create();
/** @var Collection|array<Song> $songs */
$songs = Song::orderBy('id')->take(2)->get(); $songs = Song::orderBy('id')->take(2)->get();
$songIds = array_pluck($songs->toArray(), 'id'); $songIds = array_pluck($songs->toArray(), 'id');

View file

@ -0,0 +1,39 @@
<?php
namespace Tests\Integration\Listeners;
use App\Events\SongLikeToggled;
use App\Listeners\LoveTrackOnLastfm;
use App\Models\Interaction;
use App\Models\User;
use App\Services\LastfmService;
use App\Values\LastfmLoveTrackParameters;
use Mockery;
use Tests\Feature\TestCase;
class LoveTrackOnLastFmTest extends TestCase
{
public function testHandle(): void
{
/** @var User $user */
$user = User::factory()->create(['preferences' => ['lastfm_session_key' => 'bar']]);
/** @var Interaction $interaction */
$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->getTrackName());
self::assertSame($interaction->song->artist->name, $params->getArtistName());
return true;
}),
'bar',
$interaction->liked
);
(new LoveTrackOnLastfm($lastfm))->handle(new SongLikeToggled($interaction, $user));
}
}

View file

@ -1,51 +0,0 @@
<?php
namespace Tests\Integration\Listeners;
use App\Events\SongLikeToggled;
use App\Jobs\LoveTrackOnLastfmJob;
use App\Listeners\LoveTrackOnLastfm;
use App\Models\Interaction;
use App\Models\Song;
use App\Models\User;
use App\Services\LastfmService;
use Exception;
use Illuminate\Support\Facades\Queue;
use Mockery;
use Mockery\MockInterface;
use Tests\Feature\TestCase;
class LoveTrackOnLastfmTest extends TestCase
{
/**
* @throws Exception
*/
public function testHandle(): void
{
static::createSampleMediaSet();
$user = User::factory()->create(['preferences' => ['lastfm_session_key' => 'bar']]);
$interaction = Interaction::create([
'user_id' => $user->id,
'song_id' => Song::first()->id,
]);
$queue = Queue::fake();
/** @var LastfmService|MockInterface $lastfm */
$lastfm = Mockery::mock(LastfmService::class, ['enabled' => true]);
(new LoveTrackOnLastfm($lastfm))->handle(new SongLikeToggled($interaction, $user));
$queue->assertPushed(
LoveTrackOnLastfmJob::class,
static function (LoveTrackOnLastfmJob $job) use ($interaction, $user): bool {
static::assertSame($interaction, static::getNonPublicProperty($job, 'interaction'));
static::assertSame($user, static::getNonPublicProperty($job, 'user'));
return true;
}
);
}
}

View file

@ -3,40 +3,27 @@
namespace Tests\Integration\Listeners; namespace Tests\Integration\Listeners;
use App\Events\SongStartedPlaying; use App\Events\SongStartedPlaying;
use App\Jobs\UpdateLastfmNowPlayingJob;
use App\Listeners\UpdateLastfmNowPlaying; use App\Listeners\UpdateLastfmNowPlaying;
use App\Models\Song; use App\Models\Song;
use App\Models\User; use App\Models\User;
use App\Services\LastfmService; use App\Services\LastfmService;
use Illuminate\Support\Facades\Queue;
use Mockery; use Mockery;
use Mockery\MockInterface;
use Tests\Feature\TestCase; use Tests\Feature\TestCase;
class UpdateLastfmNowPlayingTest extends TestCase class UpdateLastfmNowPlayingTest extends TestCase
{ {
public function testUpdateNowPlayingStatus(): void public function testUpdateNowPlayingStatus(): void
{ {
static::createSampleMediaSet(); /** @var User $user */
$user = User::factory()->create();
$user = User::factory()->create(['preferences' => ['lastfm_session_key' => 'bar']]); /** @var Song $song */
$song = Song::first(); $song = Song::factory()->create();
$queue = Queue::fake();
/** @var LastfmService|MockInterface $lastfm */
$lastfm = Mockery::mock(LastfmService::class, ['enabled' => true]); $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);
(new UpdateLastfmNowPlaying($lastfm))->handle(new SongStartedPlaying($song, $user)); (new UpdateLastfmNowPlaying($lastfm))->handle(new SongStartedPlaying($song, $user));
$queue->assertPushed(
UpdateLastfmNowPlayingJob::class,
static function (UpdateLastfmNowPlayingJob $job) use ($user, $song): bool {
self::assertSame($user, static::getNonPublicProperty($job, 'user'));
self::assertSame($song, static::getNonPublicProperty($job, 'song'));
return true;
}
);
} }
} }

View file

@ -3,6 +3,8 @@
namespace Tests\Integration\Services; namespace Tests\Integration\Services;
use App\Events\SongLikeToggled; use App\Events\SongLikeToggled;
use App\Events\SongsBatchLiked;
use App\Events\SongsBatchUnliked;
use App\Models\Interaction; use App\Models\Interaction;
use App\Models\Song; use App\Models\Song;
use App\Models\User; use App\Models\User;
@ -18,7 +20,7 @@ class InteractionServiceTest extends TestCase
{ {
parent::setUp(); parent::setUp();
$this->interactionService = new InteractionService(new Interaction()); $this->interactionService = new InteractionService();
} }
public function testIncreasePlayCount(): void public function testIncreasePlayCount(): void
@ -47,7 +49,7 @@ class InteractionServiceTest extends TestCase
public function testLikeMultipleSongs(): void public function testLikeMultipleSongs(): void
{ {
$this->expectsEvents(SongLikeToggled::class); $this->expectsEvents(SongsBatchLiked::class);
/** @var Collection $songs */ /** @var Collection $songs */
$songs = Song::factory(2)->create(); $songs = Song::factory(2)->create();
@ -64,8 +66,9 @@ class InteractionServiceTest extends TestCase
public function testUnlikeMultipleSongs(): void public function testUnlikeMultipleSongs(): void
{ {
$this->expectsEvents(SongLikeToggled::class); $this->expectsEvents(SongsBatchUnliked::class);
/** @var User $user */
$user = User::factory()->create(); $user = User::factory()->create();
/** @var Collection $interactions */ /** @var Collection $interactions */

View file

@ -1,41 +0,0 @@
<?php
namespace Tests\Unit\Jobs;
use App\Jobs\LoveTrackOnLastfmJob;
use App\Models\Interaction;
use App\Models\User;
use App\Services\LastfmService;
use Mockery;
use Tests\TestCase;
class LoveTrackOnLastfmJobTest extends TestCase
{
private $job;
private $user;
private $interaction;
public function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create(['preferences' => ['lastfm_session_key' => 'foo']]);
$this->interaction = Interaction::factory()->make();
$this->job = new LoveTrackOnLastfmJob($this->user, $this->interaction);
}
public function testHandle(): void
{
$lastFm = Mockery::mock(LastfmService::class);
$lastFm->shouldReceive('toggleLoveTrack')
->once()
->with(
$this->interaction->song->title,
$this->interaction->song->artist->name,
'foo',
$this->interaction->liked
);
$this->job->handle($lastFm);
}
}

View file

@ -1,42 +0,0 @@
<?php
namespace Tests\Unit\Jobs;
use App\Jobs\UpdateLastfmNowPlayingJob;
use App\Models\Song;
use App\Models\User;
use App\Services\LastfmService;
use Mockery;
use Tests\TestCase;
class UpdateLastfmNowPlayingJobTest extends TestCase
{
private $job;
private $user;
private $song;
public function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create(['preferences' => ['lastfm_session_key' => 'foo']]);
$this->song = Song::factory()->make();
$this->job = new UpdateLastfmNowPlayingJob($this->user, $this->song);
}
public function testHandle(): void
{
$lastFm = Mockery::mock(LastfmService::class);
$lastFm->shouldReceive('updateNowPlaying')
->once()
->with(
$this->song->artist->name,
$this->song->title,
$this->song->album->name,
$this->song->length,
'foo'
);
$this->job->handle($lastFm);
}
}