mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat(lastfm): batch like/unlike are now asynchronous
This commit is contained in:
parent
6c24b529cc
commit
ef1add3877
28 changed files with 358 additions and 359 deletions
21
app/Events/SongsBatchLiked.php
Normal file
21
app/Events/SongsBatchLiked.php
Normal 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;
|
||||
}
|
||||
}
|
21
app/Events/SongsBatchUnliked.php
Normal file
21
app/Events/SongsBatchUnliked.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ class ScrobbleController extends Controller
|
|||
public function store(ScrobbleStoreRequest $request, Song $song)
|
||||
{
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
29
app/Listeners/LoveMultipleTracksOnLastfm.php
Normal file
29
app/Listeners/LoveMultipleTracksOnLastfm.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,10 +3,11 @@
|
|||
namespace App\Listeners;
|
||||
|
||||
use App\Events\SongLikeToggled;
|
||||
use App\Jobs\LoveTrackOnLastfmJob;
|
||||
use App\Services\LastfmService;
|
||||
use App\Values\LastfmLoveTrackParameters;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class LoveTrackOnLastfm
|
||||
class LoveTrackOnLastfm implements ShouldQueue
|
||||
{
|
||||
private $lastfm;
|
||||
|
||||
|
@ -25,6 +26,13 @@ class LoveTrackOnLastfm
|
|||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
22
app/Listeners/UnloveMultipleTracksOnLastfm.php
Normal file
22
app/Listeners/UnloveMultipleTracksOnLastfm.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -3,10 +3,10 @@
|
|||
namespace App\Listeners;
|
||||
|
||||
use App\Events\SongStartedPlaying;
|
||||
use App\Jobs\UpdateLastfmNowPlayingJob;
|
||||
use App\Services\LastfmService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class UpdateLastfmNowPlaying
|
||||
class UpdateLastfmNowPlaying implements ShouldQueue
|
||||
{
|
||||
private $lastfm;
|
||||
|
||||
|
@ -21,6 +21,12 @@ class UpdateLastfmNowPlaying
|
|||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
* @property User $user
|
||||
* @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 Builder whereSongIdAndUserId(string $songId, string $userId)
|
||||
* @method static Builder whereIn(...$params)
|
||||
*/
|
||||
class Interaction extends Model
|
||||
{
|
||||
|
|
|
@ -36,7 +36,7 @@ use Laravel\Scout\Searchable;
|
|||
* @method static self first()
|
||||
* @method static EloquentCollection orderBy(...$args)
|
||||
* @method static int count()
|
||||
* @method static self|null find($id)
|
||||
* @method static self|Collection|null find($id)
|
||||
* @method static Builder take(int $count)
|
||||
*/
|
||||
class Song extends Model
|
||||
|
|
|
@ -7,12 +7,16 @@ use App\Events\ArtistInformationFetched;
|
|||
use App\Events\LibraryChanged;
|
||||
use App\Events\MediaCacheObsolete;
|
||||
use App\Events\SongLikeToggled;
|
||||
use App\Events\SongsBatchLiked;
|
||||
use App\Events\SongsBatchUnliked;
|
||||
use App\Events\SongStartedPlaying;
|
||||
use App\Listeners\ClearMediaCache;
|
||||
use App\Listeners\DownloadAlbumCover;
|
||||
use App\Listeners\DownloadArtistImage;
|
||||
use App\Listeners\LoveMultipleTracksOnLastfm;
|
||||
use App\Listeners\LoveTrackOnLastfm;
|
||||
use App\Listeners\TidyLibrary;
|
||||
use App\Listeners\UnloveMultipleTracksOnLastfm;
|
||||
use App\Listeners\UpdateLastfmNowPlaying;
|
||||
use App\Models\Album;
|
||||
use App\Models\Song;
|
||||
|
@ -32,6 +36,14 @@ class EventServiceProvider extends ServiceProvider
|
|||
LoveTrackOnLastfm::class,
|
||||
],
|
||||
|
||||
SongsBatchLiked::class => [
|
||||
LoveMultipleTracksOnLastfm::class,
|
||||
],
|
||||
|
||||
SongsBatchUnliked::class => [
|
||||
UnloveMultipleTracksOnLastfm::class,
|
||||
],
|
||||
|
||||
SongStartedPlaying::class => [
|
||||
UpdateLastfmNowPlaying::class,
|
||||
],
|
||||
|
|
|
@ -4,21 +4,45 @@ namespace App\Services;
|
|||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
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 get($uri, ...$args)
|
||||
* @method object post($uri, ...$data)
|
||||
* @method object put($uri, ...$data)
|
||||
* @method object patch($uri, ...$data)
|
||||
* @method object head($uri, ...$data)
|
||||
* @method object delete($uri)
|
||||
* @method object 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 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
|
||||
{
|
||||
private const MAGIC_METHODS = [
|
||||
'get',
|
||||
'post',
|
||||
'put',
|
||||
'patch',
|
||||
'head',
|
||||
'delete',
|
||||
'getAsync',
|
||||
'postAsync',
|
||||
'putAsync',
|
||||
'patchAsync',
|
||||
'headAsync',
|
||||
'deleteAsync',
|
||||
];
|
||||
|
||||
protected $responseFormat = 'json';
|
||||
protected $client;
|
||||
protected $cache;
|
||||
|
@ -50,7 +74,7 @@ abstract class AbstractApiClient
|
|||
* an "API signature" of the request. Appending an API key will break the request.
|
||||
* @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 = [])
|
||||
{
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -81,19 +110,25 @@ abstract class AbstractApiClient
|
|||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return mixed|SimpleXMLElement|null
|
||||
* @return mixed|SimpleXMLElement|void
|
||||
*/
|
||||
public function __call(string $method, array $args)
|
||||
{
|
||||
Assert::inArray($method, self::MAGIC_METHODS);
|
||||
|
||||
if (count($args) < 1) {
|
||||
throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
|
||||
}
|
||||
|
||||
$uri = $args[0];
|
||||
$opts = $args[1] ?? [];
|
||||
$params = $args[1] ?? [];
|
||||
$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 (parse_url($uri, PHP_URL_QUERY)) {
|
||||
$uri .= "&{$this->keyParam}=" . $this->getKey();
|
||||
$uri .= "&$this->keyParam=" . $this->getKey();
|
||||
} else {
|
||||
$uri .= "?{$this->keyParam}=" . $this->getKey();
|
||||
$uri .= "?$this->keyParam=" . $this->getKey();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,18 +3,15 @@
|
|||
namespace App\Services;
|
||||
|
||||
use App\Events\SongLikeToggled;
|
||||
use App\Events\SongsBatchLiked;
|
||||
use App\Events\SongsBatchUnliked;
|
||||
use App\Models\Interaction;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InteractionService
|
||||
{
|
||||
private $interaction;
|
||||
|
||||
public function __construct(Interaction $interaction)
|
||||
{
|
||||
$this->interaction = $interaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return tap($this->interaction->firstOrCreate([
|
||||
return tap(Interaction::firstOrCreate([
|
||||
'song_id' => $songId,
|
||||
'user_id' => $user->id,
|
||||
]), static function (Interaction $interaction): void {
|
||||
|
@ -42,7 +39,7 @@ class InteractionService
|
|||
*/
|
||||
public function toggleLike(string $songId, User $user): Interaction
|
||||
{
|
||||
return tap($this->interaction->firstOrCreate([
|
||||
return tap(Interaction::firstOrCreate([
|
||||
'song_id' => $songId,
|
||||
'user_id' => $user->id,
|
||||
]), static function (Interaction $interaction): void {
|
||||
|
@ -60,10 +57,10 @@ class InteractionService
|
|||
*
|
||||
* @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 {
|
||||
return tap($this->interaction->firstOrCreate([
|
||||
$interactions = collect($songIds)->map(static function ($songId) use ($user): Interaction {
|
||||
return tap(Interaction::firstOrCreate([
|
||||
'song_id' => $songId,
|
||||
'user_id' => $user->id,
|
||||
]), static function (Interaction $interaction): void {
|
||||
|
@ -73,10 +70,14 @@ class InteractionService
|
|||
|
||||
$interaction->liked = true;
|
||||
$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
|
||||
{
|
||||
$this->interaction
|
||||
->whereIn('song_id', $songIds)
|
||||
Interaction::whereIn('song_id', $songIds)
|
||||
->where('user_id', $user->id)
|
||||
->get()
|
||||
->each(
|
||||
static function (Interaction $interaction): void {
|
||||
$interaction->liked = false;
|
||||
$interaction->save();
|
||||
->update(['liked' => false]);
|
||||
|
||||
event(new SongLikeToggled($interaction));
|
||||
}
|
||||
);
|
||||
event(new SongsBatchUnliked(Song::find($songIds), $user));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Values\LastfmLoveTrackParameters;
|
||||
use GuzzleHttp\Promise\Promise;
|
||||
use GuzzleHttp\Promise\Utils;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
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';
|
||||
|
||||
|
@ -27,11 +31,7 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
|
|||
return $this->getKey() && $this->getSecret();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about an artist.
|
||||
*
|
||||
* @return array<mixed>|null
|
||||
*/
|
||||
/** @return array<mixed>|null */
|
||||
public function getArtistInformation(string $name): ?array
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
|
@ -76,11 +76,7 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about an album.
|
||||
*
|
||||
* @return array<mixed>|null
|
||||
*/
|
||||
/** @return array<mixed>|null */
|
||||
public function getAlbumInformation(string $albumName, string $artistName): ?array
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
|
@ -159,25 +155,25 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrobble a song.
|
||||
*
|
||||
* @param string $artist The artist name
|
||||
* @param string $track The track name
|
||||
* @param string|int $timestamp The UNIX timestamp
|
||||
* @param string $album The album name
|
||||
* @param string $sk The session key
|
||||
*/
|
||||
public function scrobble(string $artist, string $track, $timestamp, string $album, string $sk): void
|
||||
{
|
||||
$params = compact('artist', 'track', 'timestamp', 'sk');
|
||||
public function scrobble(
|
||||
string $artistName,
|
||||
string $trackName,
|
||||
$timestamp,
|
||||
string $albumName,
|
||||
string $sessionKey
|
||||
): void {
|
||||
$params = [
|
||||
'artist' => $artistName,
|
||||
'track' => $trackName,
|
||||
'timestamp' => $timestamp,
|
||||
'sk' => $sessionKey,
|
||||
'method' => 'track.scrobble',
|
||||
];
|
||||
|
||||
if ($album) {
|
||||
$params['album'] = $album;
|
||||
if ($albumName) {
|
||||
$params['album'] = $albumName;
|
||||
}
|
||||
|
||||
$params['method'] = 'track.scrobble';
|
||||
|
||||
try {
|
||||
$this->post('/', $this->buildAuthCallParams($params), false);
|
||||
} catch (Throwable $e) {
|
||||
|
@ -185,42 +181,63 @@ class LastfmService extends AbstractApiClient implements ApiConsumerInterface
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public function toggleLoveTrack(LastfmLoveTrackParameters $params, string $sessionKey, bool $love = true): void
|
||||
{
|
||||
$params = compact('track', 'artist', 'sk');
|
||||
$params['method'] = $love ? 'track.love' : 'track.unlove';
|
||||
|
||||
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) {
|
||||
$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 string $sk The session key
|
||||
*/
|
||||
public function updateNowPlaying(string $artist, string $track, string $album, $duration, string $sk): void
|
||||
{
|
||||
$params = compact('artist', 'track', 'duration', 'sk');
|
||||
$params['method'] = 'track.updateNowPlaying';
|
||||
public function updateNowPlaying(
|
||||
string $artistName,
|
||||
string $trackName,
|
||||
string $albumName,
|
||||
$duration,
|
||||
string $sessionKey
|
||||
): void {
|
||||
$params = [
|
||||
'artist' => $artistName,
|
||||
'track' => $trackName,
|
||||
'duration' => $duration,
|
||||
'sk' => $sessionKey,
|
||||
'method' => 'track.updateNowPlaying',
|
||||
];
|
||||
|
||||
if ($album) {
|
||||
$params['album'] = $album;
|
||||
if ($albumName) {
|
||||
$params['album'] = $albumName;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
30
app/Values/LastfmLoveTrackParameters.php
Normal file
30
app/Values/LastfmLoveTrackParameters.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -29,7 +29,8 @@
|
|||
"lstrojny/functional-php": "^1.14",
|
||||
"teamtnt/laravel-scout-tntsearch-driver": "^11.1",
|
||||
"algolia/algoliasearch-client-php": "^2.7",
|
||||
"laravel/ui": "^3.2"
|
||||
"laravel/ui": "^3.2",
|
||||
"webmozart/assert": "^1.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"facade/ignition": "^2.5",
|
||||
|
|
2
composer.lock
generated
2
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "5f54c3d8584004c6454de204fd4c4509",
|
||||
"content-hash": "058ece2df4d81093f9268a416cb1d0ef",
|
||||
"packages": [
|
||||
{
|
||||
"name": "algolia/algoliasearch-client-php",
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Database\Factories;
|
|||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
|
@ -18,8 +19,10 @@ class UserFactory extends Factory
|
|||
'email' => $this->faker->email,
|
||||
'password' => Hash::make('secret'),
|
||||
'is_admin' => false,
|
||||
'preferences' => [],
|
||||
'remember_token' => str_random(10),
|
||||
'preferences' => [
|
||||
'lastfm_session_key' => Str::random(),
|
||||
],
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
namespace Tests\Feature;
|
||||
|
||||
use App\Events\SongLikeToggled;
|
||||
use App\Events\SongsBatchLiked;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InteractionTest extends TestCase
|
||||
{
|
||||
|
@ -66,10 +68,12 @@ class InteractionTest extends TestCase
|
|||
|
||||
public function testToggleBatch(): void
|
||||
{
|
||||
$this->expectsEvents(SongLikeToggled::class);
|
||||
$this->expectsEvents(SongsBatchLiked::class);
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
/** @var Collection|array<Song> $songs */
|
||||
$songs = Song::orderBy('id')->take(2)->get();
|
||||
$songIds = array_pluck($songs->toArray(), 'id');
|
||||
|
||||
|
|
39
tests/Integration/Listeners/LoveTrackOnLastFmTest.php
Normal file
39
tests/Integration/Listeners/LoveTrackOnLastFmTest.php
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,40 +3,27 @@
|
|||
namespace Tests\Integration\Listeners;
|
||||
|
||||
use App\Events\SongStartedPlaying;
|
||||
use App\Jobs\UpdateLastfmNowPlayingJob;
|
||||
use App\Listeners\UpdateLastfmNowPlaying;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\LastfmService;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\Feature\TestCase;
|
||||
|
||||
class UpdateLastfmNowPlayingTest extends TestCase
|
||||
{
|
||||
public function testUpdateNowPlayingStatus(): void
|
||||
{
|
||||
static::createSampleMediaSet();
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user = User::factory()->create(['preferences' => ['lastfm_session_key' => 'bar']]);
|
||||
$song = Song::first();
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->create();
|
||||
|
||||
$queue = Queue::fake();
|
||||
|
||||
/** @var LastfmService|MockInterface $lastfm */
|
||||
$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));
|
||||
|
||||
$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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
namespace Tests\Integration\Services;
|
||||
|
||||
use App\Events\SongLikeToggled;
|
||||
use App\Events\SongsBatchLiked;
|
||||
use App\Events\SongsBatchUnliked;
|
||||
use App\Models\Interaction;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
|
@ -18,7 +20,7 @@ class InteractionServiceTest extends TestCase
|
|||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->interactionService = new InteractionService(new Interaction());
|
||||
$this->interactionService = new InteractionService();
|
||||
}
|
||||
|
||||
public function testIncreasePlayCount(): void
|
||||
|
@ -47,7 +49,7 @@ class InteractionServiceTest extends TestCase
|
|||
|
||||
public function testLikeMultipleSongs(): void
|
||||
{
|
||||
$this->expectsEvents(SongLikeToggled::class);
|
||||
$this->expectsEvents(SongsBatchLiked::class);
|
||||
|
||||
/** @var Collection $songs */
|
||||
$songs = Song::factory(2)->create();
|
||||
|
@ -64,8 +66,9 @@ class InteractionServiceTest extends TestCase
|
|||
|
||||
public function testUnlikeMultipleSongs(): void
|
||||
{
|
||||
$this->expectsEvents(SongLikeToggled::class);
|
||||
$this->expectsEvents(SongsBatchUnliked::class);
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
/** @var Collection $interactions */
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue