Skip to content

Commit

Permalink
Add NowPlayingUpdate api route
Browse files Browse the repository at this point in the history
This route updates the current playlist state and updates the users'
playback history
  • Loading branch information
usox committed May 16, 2022
1 parent bb883da commit 9b9373b
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 19 deletions.
30 changes: 30 additions & 0 deletions resource/api-schema/NowPlayingUpdate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://uxmp/NowPlayingUpdate.json",
"title": "Update NowPlaying state",
"description": "Defines the nowplaying state request for the user",
"type": "object",
"properties": {
"songId": {
"type": "integer",
"description": "The current song id"
},
"temporaryPlaylist": {
"type": "object",
"description": "Playlist id, if available",
"properties": {
"id": {
"type": ["string", "null"],
"description": "The id of the temporary playlist"
},
"offset": {
"type": "integer",
"description": "The position of the song in the playlist"
}
},
"required": ["id", "offset"]
}
},
"additionalProperties": false,
"required": ["songId", "temporaryPlaylist"]
}
3 changes: 2 additions & 1 deletion src/Api/ApiApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public function run(
$app->get('/play/history', Playback\PlaybackHistoryApplication::class);
$app->get('/play/mostplayed', MostPlayedApplication::class);
$app->get('/play/{id}', Playback\PlaySongApplication::class);
$app->post('/play/nowplaying', Playback\NowPlayingUpdate::class);

// artists
$app->get('/artists', Artist\ArtistListApplication::class);
Expand Down Expand Up @@ -124,7 +125,7 @@ public function run(

// temporary playlist
$app->get('/temporary_playlist', TemporaryPlaylist\TemporaryPlaylistRetrieveApplication::class);
$app->get('/temporary_playlist/{temporaryPlaylistId}/songs', TemporaryPlaylist\TemporaryPlaylistRetrieveSongsApplication::class);
$app->get('/temporary_playlist/{temporaryPlaylistId}', TemporaryPlaylist\TemporaryPlaylistRetrieveSongsApplication::class);
$app->post('/temporary_playlist', TemporaryPlaylist\TemporaryPlaylistUpdateApplication::class);

// playlist types
Expand Down
79 changes: 79 additions & 0 deletions src/Api/Playback/NowPlayingUpdate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Uxmp\Core\Api\Playback;

use DateTime;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Uxmp\Core\Api\AbstractApiApplication;
use Uxmp\Core\Api\Lib\SchemaValidatorInterface;
use Uxmp\Core\Component\Authentication\SessionValidatorMiddleware;
use Uxmp\Core\Orm\Repository\PlaybackHistoryRepositoryInterface;
use Uxmp\Core\Orm\Repository\SongRepositoryInterface;
use Uxmp\Core\Orm\Repository\TemporaryPlaylistRepositoryInterface;

/**
* Adds the song to the users' playback history and updates the temporary playlist
*/
final class NowPlayingUpdate extends AbstractApiApplication
{
/**
* @param SchemaValidatorInterface<array{
* songId: int,
* temporaryPlaylist: array{
* id: string|null,
* offset: int
* }
* }> $schemaValidator
*/
public function __construct(
private readonly SchemaValidatorInterface $schemaValidator,
private readonly TemporaryPlaylistRepositoryInterface $temporaryPlaylistRepository,
private readonly PlaybackHistoryRepositoryInterface $playbackHistoryRepository,
private readonly SongRepositoryInterface $songRepository,
) {
}

protected function run(
ServerRequestInterface $request,
ResponseInterface $response,
array $args
): ResponseInterface {
$body = $this->schemaValidator->getValidatedBody(
$request,
'NowPlayingUpdate.json',
);
$user = $request->getAttribute(SessionValidatorMiddleware::USER);

$temporaryPlaylist = $this->temporaryPlaylistRepository->findOneBy([
'id' => $body['temporaryPlaylist']['id'],
'owner' => $user,
]);

if ($temporaryPlaylist !== null) {
$temporaryPlaylist->setOffset($body['temporaryPlaylist']['offset']);

$this->temporaryPlaylistRepository->save($temporaryPlaylist);
}

$song = $this->songRepository->find($body['songId']);

if ($song !== null) {
$history = $this->playbackHistoryRepository->prototype()
->setUser($user)
->setSong($song)
->setPlayDate(new DateTime());

$this->playbackHistoryRepository->save($history);
}

return $this->asJson(
$response,
[
'result' => true,
]
);
}
}
17 changes: 2 additions & 15 deletions src/Api/Playback/PlaySongApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
use Uxmp\Core\Api\AbstractApiApplication;
use Uxmp\Core\Component\Authentication\SessionValidatorMiddleware;
use Uxmp\Core\Orm\Model\UserInterface;
use Uxmp\Core\Orm\Repository\PlaybackHistoryRepositoryInterface;
use Uxmp\Core\Orm\Repository\SongRepositoryInterface;

final class PlaySongApplication extends AbstractApiApplication
{
public function __construct(
private readonly Psr17Factory $psr17Factory,
private readonly SongRepositoryInterface $songRepository,
private readonly PlaybackHistoryRepositoryInterface $playbackHistoryRepository,
) {
}

Expand All @@ -32,19 +29,9 @@ protected function run(
$song = $this->songRepository->find($songId);

if ($song === null) {
throw new \RuntimeException('song not found');
throw new RuntimeException('song not found');
}

/** @var UserInterface $user */
$user = $request->getAttribute(SessionValidatorMiddleware::USER);

$history = $this->playbackHistoryRepository->prototype()
->setUser($user)
->setSong($song)
->setPlayDate(new \DateTime());

$this->playbackHistoryRepository->save($history);

$path = $song->getFilename();

$size = filesize($path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ protected function run(
return $this->asJson(
$response,
[
'items' => $songs,
'offset' => $temporaryPlaylist->getOffset(),
'songs' => $songs,
]
);
}
Expand Down
5 changes: 4 additions & 1 deletion tests/Api/ApiApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ public function testRunRuns(): void
$app->shouldReceive('get')
->with('/play/{id}', Playback\PlaySongApplication::class)
->once();
$app->shouldReceive('post')
->with('/play/nowplaying', Playback\NowPlayingUpdate::class)
->once();
$app->shouldReceive('get')
->with('/artists', Artist\ArtistListApplication::class)
->once();
Expand Down Expand Up @@ -219,7 +222,7 @@ public function testRunRuns(): void
->with('/temporary_playlist', TemporaryPlaylist\TemporaryPlaylistRetrieveApplication::class)
->once();
$app->shouldReceive('get')
->with('/temporary_playlist/{temporaryPlaylistId}/songs', TemporaryPlaylist\TemporaryPlaylistRetrieveSongsApplication::class)
->with('/temporary_playlist/{temporaryPlaylistId}', TemporaryPlaylist\TemporaryPlaylistRetrieveSongsApplication::class)
->once();
$app->shouldReceive('post')
->with('/temporary_playlist', TemporaryPlaylist\TemporaryPlaylistUpdateApplication::class)
Expand Down
138 changes: 138 additions & 0 deletions tests/Api/Playback/NowPlayingUpdateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

namespace Uxmp\Core\Api\Playback;

use DateTime;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use Mockery\MockInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Uxmp\Core\Api\Lib\SchemaValidatorInterface;
use Uxmp\Core\Component\Authentication\SessionValidatorMiddleware;
use Uxmp\Core\Orm\Model\PlaybackHistoryInterface;
use Uxmp\Core\Orm\Model\SongInterface;
use Uxmp\Core\Orm\Model\TemporaryPlaylistInterface;
use Uxmp\Core\Orm\Model\UserInterface;
use Uxmp\Core\Orm\Repository\PlaybackHistoryRepositoryInterface;
use Uxmp\Core\Orm\Repository\SongRepositoryInterface;
use Uxmp\Core\Orm\Repository\TemporaryPlaylistRepositoryInterface;

class NowPlayingUpdateTest extends MockeryTestCase
{
private MockInterface $schemaValidator;

private MockInterface $temporaryPlaylistRepository;

private MockInterface $playbackHistoryRepository;

private MockInterface $songRepository;

private NowPlayingUpdate $subject;

public function setUp(): void
{
$this->schemaValidator = Mockery::mock(SchemaValidatorInterface::class);
$this->temporaryPlaylistRepository = Mockery::mock(TemporaryPlaylistRepositoryInterface::class);
$this->playbackHistoryRepository = Mockery::mock(PlaybackHistoryRepositoryInterface::class);
$this->songRepository = Mockery::mock(SongRepositoryInterface::class);

$this->subject = new NowPlayingUpdate(
$this->schemaValidator,
$this->temporaryPlaylistRepository,
$this->playbackHistoryRepository,
$this->songRepository,
);
}

public function testInvokeReturnsData(): void
{
$request = Mockery::mock(ServerRequestInterface::class);
$response = Mockery::mock(ResponseInterface::class);
$user = Mockery::mock(UserInterface::class);
$temporaryPlaylist = Mockery::mock(TemporaryPlaylistInterface::class);
$song = Mockery::mock(SongInterface::class);
$history = Mockery::mock(PlaybackHistoryInterface::class);
$stream = Mockery::mock(StreamInterface::class);

$playlistId = 'some-id';
$offset = 666;
$songId = 42;

$request->shouldReceive('getAttribute')
->with(SessionValidatorMiddleware::USER)
->once()
->andReturn($user);

$this->temporaryPlaylistRepository->shouldReceive('findOneBy')
->with([
'id' => $playlistId,
'owner' => $user,
])
->once()
->andReturn($temporaryPlaylist);

$this->schemaValidator->shouldReceive('getValidatedBody')
->with($request, 'NowPlayingUpdate.json')
->once()
->andReturn([
'temporaryPlaylist' => [
'id' => $playlistId,
'offset' => $offset,
],
'songId' => $songId,
]);
$this->temporaryPlaylistRepository->shouldReceive('save')
->with($temporaryPlaylist)
->once();

$temporaryPlaylist->shouldReceive('setOffset')
->with($offset)
->once();

$this->songRepository->shouldReceive('find')
->with($songId)
->once()
->andReturn($song);

$this->playbackHistoryRepository->shouldReceive('prototype->setUser')
->with($user)
->once()
->andReturn($history);
$this->playbackHistoryRepository->shouldReceive('save')
->with($history)
->once();

$history->shouldReceive('setSong')
->with($song)
->once()
->andReturnSelf();
$history->shouldReceive('setPlayDate')
->with(Mockery::type(DateTime::class))
->once()
->andReturnSelf();

$response->shouldReceive('getBody')
->withNoArgs()
->once()
->andReturn($stream);
$response->shouldReceive('withHeader')
->with('Content-Type', 'application/json')
->once()
->andReturnSelf();

$stream->shouldReceive('write')
->with(json_encode([
'result' => true,
], JSON_PRETTY_PRINT))
->once();

$this->assertSame(
$response,
call_user_func($this->subject, $request, $response, [])
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function testRunReturnsResult(): void
$songId1 = 666;
$songId2 = 42;
$songListResult = 'some-result';
$offset = 42;

$request->shouldReceive('getAttribute')
->with(SessionValidatorMiddleware::USER)
Expand All @@ -74,6 +75,10 @@ public function testRunReturnsResult(): void
->withNoArgs()
->once()
->andReturn([$songId1, $songId2,]);
$temporaryPlaylist->shouldReceive('getOffset')
->withNoArgs()
->once()
->andReturn($offset);

$this->songRepository->shouldReceive('find')
->with($songId1)
Expand Down Expand Up @@ -109,7 +114,7 @@ public function testRunReturnsResult(): void
->andReturn($songListResult);

$stream->shouldReceive('write')
->with(json_encode(['items' => [$songListResult]], JSON_PRETTY_PRINT))
->with(json_encode(['offset' => $offset, 'songs' => [$songListResult]], JSON_PRETTY_PRINT))
->once();

$this->assertSame(
Expand Down

0 comments on commit 9b9373b

Please sign in to comment.