From 4e6f1d700c543830e98d92257d5e0eacdb54c87b Mon Sep 17 00:00:00 2001 From: "Buster \"Silver Eagle\" Neece" Date: Sat, 17 Apr 2021 13:42:30 -0500 Subject: [PATCH] Reimplement playlist queue to be attached to StationPlaylistMedia table. (#4031) --- .../Stations/Playlists/DeleteQueueAction.php | 6 +- .../Api/Stations/Playlists/GetQueueAction.php | 3 +- .../Stations/Playlists/ReshuffleAction.php | 12 +- src/Entity/Api/StationPlaylistQueue.php | 46 +++ .../Migration/Version20210416214621.php | 33 +++ .../StationPlaylistMediaRepository.php | 126 +++++--- .../Repository/StationRequestRepository.php | 14 +- src/Entity/StationMedia.php | 16 -- src/Entity/StationPlaylist.php | 67 ----- src/Entity/StationPlaylistMedia.php | 8 + src/Radio/AutoDJ/Queue.php | 271 ++++++++---------- 11 files changed, 321 insertions(+), 281 deletions(-) create mode 100644 src/Entity/Api/StationPlaylistQueue.php create mode 100644 src/Entity/Migration/Version20210416214621.php diff --git a/src/Controller/Api/Stations/Playlists/DeleteQueueAction.php b/src/Controller/Api/Stations/Playlists/DeleteQueueAction.php index dc6ee5b821..84c10ee7d8 100644 --- a/src/Controller/Api/Stations/Playlists/DeleteQueueAction.php +++ b/src/Controller/Api/Stations/Playlists/DeleteQueueAction.php @@ -12,14 +12,12 @@ class DeleteQueueAction extends AbstractPlaylistsAction public function __invoke( ServerRequest $request, Response $response, + Entity\Repository\StationPlaylistMediaRepository $spmRepo, $id ): ResponseInterface { $record = $this->requireRecord($request->getStation(), $id); - $record->setQueue(null); - $this->em->persist($record); - - $this->em->flush(); + $spmRepo->resetQueue($record); return $response->withJson( new Entity\Api\Status( diff --git a/src/Controller/Api/Stations/Playlists/GetQueueAction.php b/src/Controller/Api/Stations/Playlists/GetQueueAction.php index 5fb4c9ae58..5ca0ac8570 100644 --- a/src/Controller/Api/Stations/Playlists/GetQueueAction.php +++ b/src/Controller/Api/Stations/Playlists/GetQueueAction.php @@ -13,6 +13,7 @@ class GetQueueAction extends AbstractPlaylistsAction public function __invoke( ServerRequest $request, Response $response, + Entity\Repository\StationPlaylistMediaRepository $spmRepo, $id ): ResponseInterface { $record = $this->requireRecord($request->getStation(), $id); @@ -25,7 +26,7 @@ public function __invoke( throw new \InvalidArgumentException('This playlist is always shuffled and has no visible queue.'); } - $queue = (array)$record->getQueue(); + $queue = $spmRepo->getQueue($record); $paginator = Paginator::fromArray($queue, $request); return $paginator->write($response); diff --git a/src/Controller/Api/Stations/Playlists/ReshuffleAction.php b/src/Controller/Api/Stations/Playlists/ReshuffleAction.php index 6419447dc4..1db87e88a4 100644 --- a/src/Controller/Api/Stations/Playlists/ReshuffleAction.php +++ b/src/Controller/Api/Stations/Playlists/ReshuffleAction.php @@ -9,13 +9,15 @@ class ReshuffleAction extends AbstractPlaylistsAction { - public function __invoke(ServerRequest $request, Response $response, $id): ResponseInterface - { + public function __invoke( + ServerRequest $request, + Response $response, + Entity\Repository\StationPlaylistMediaRepository $spmRepo, + $id + ): ResponseInterface { $record = $this->requireRecord($request->getStation(), $id); - $record->setQueue(null); - $this->em->persist($record); - $this->em->flush(); + $spmRepo->resetQueue($record); return $response->withJson( new Entity\Api\Status( diff --git a/src/Entity/Api/StationPlaylistQueue.php b/src/Entity/Api/StationPlaylistQueue.php new file mode 100644 index 0000000000..667c1429e4 --- /dev/null +++ b/src/Entity/Api/StationPlaylistQueue.php @@ -0,0 +1,46 @@ +addSql('ALTER TABLE station_playlist_media ADD is_queued TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE station_playlists DROP queue'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE station_playlist_media DROP is_queued'); + $this->addSql('ALTER TABLE station_playlists ADD queue LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci` COMMENT \'(DC2Type:array)\''); + } +} diff --git a/src/Entity/Repository/StationPlaylistMediaRepository.php b/src/Entity/Repository/StationPlaylistMediaRepository.php index 1fe6810eeb..b2ab7c615b 100644 --- a/src/Entity/Repository/StationPlaylistMediaRepository.php +++ b/src/Entity/Repository/StationPlaylistMediaRepository.php @@ -72,10 +72,6 @@ public function addMediaToPlaylist( $record = new Entity\StationPlaylistMedia($playlist, $media); $record->setWeight($weight); $this->em->persist($record); - - // Add the newly added song into the cached queue. - $playlist->addToQueue($media); - $this->em->persist($playlist); } return $weight; @@ -124,10 +120,6 @@ function (Entity\StationPlaylistMedia $spm) use ($station) { foreach ($playlists as $spmRow) { $playlist = $spmRow->getPlaylist(); - - $playlist->removeFromQueue($media); - $this->em->persist($playlist); - $affectedPlaylists[$playlist->getId()] = $playlist; $this->queueRepo->clearForMediaAndPlaylist($media, $playlist); @@ -159,43 +151,109 @@ public function setMediaOrder(Entity\StationPlaylist $playlist, $mapping): void DQL )->setParameter('playlist_id', $playlist->getId()); - foreach ($mapping as $id => $weight) { - $update_query->setParameter('id', $id) - ->setParameter('weight', $weight) + $this->em->transactional( + function () use ($update_query, $mapping): void { + foreach ($mapping as $id => $weight) { + $update_query->setParameter('id', $id) + ->setParameter('weight', $weight) + ->execute(); + } + } + ); + } + + /** + * @return Entity\Api\StationPlaylistQueue[] + */ + public function resetQueue(StationPlaylist $playlist): array + { + if ($playlist::SOURCE_SONGS !== $playlist->getSource()) { + throw new \InvalidArgumentException('Playlist must contain songs.'); + } + + if ($playlist::ORDER_SEQUENTIAL === $playlist->getOrder()) { + $this->em->createQuery( + <<<'DQL' + UPDATE App\Entity\StationPlaylistMedia spm + SET spm.is_queued = 1 + WHERE spm.playlist = :playlist + DQL + )->setParameter('playlist', $playlist) ->execute(); + } elseif ($playlist::ORDER_SHUFFLE === $playlist->getOrder()) { + $this->em->transactional( + function () use ($playlist): void { + $allSpmRecordsQuery = $this->em->createQuery( + <<<'DQL' + SELECT spm.id + FROM App\Entity\StationPlaylistMedia spm + WHERE spm.playlist = :playlist + ORDER BY RAND() + DQL + )->setParameter('playlist', $playlist); + + $updateSpmWeightQuery = $this->em->createQuery( + <<<'DQL' + UPDATE App\Entity\StationPlaylistMedia spm + SET spm.weight=:weight, spm.is_queued=1 + WHERE spm.id = :id + DQL + ); + + $allSpmRecords = $allSpmRecordsQuery->toIterable([], $allSpmRecordsQuery::HYDRATE_SCALAR); + $weight = 1; + + foreach ($allSpmRecords as $spmId) { + $updateSpmWeightQuery->setParameter('id', $spmId) + ->setParameter('weight', $weight) + ->execute(); + + $weight++; + } + } + ); } - // Clear the playback queue. - $playlist->setQueue($this->getPlayableMedia($playlist)); - $this->em->persist($playlist); - $this->em->flush(); + return $this->getQueue($playlist); } /** - * @return mixed[] + * @return Entity\Api\StationPlaylistQueue[] */ - public function getPlayableMedia(Entity\StationPlaylist $playlist): array + public function getQueue(StationPlaylist $playlist): array { - $all_media = $this->em->createQuery( - <<<'DQL' - SELECT sm.id, sm.song_id, sm.artist, sm.title - FROM App\Entity\StationMedia sm - JOIN sm.playlists spm - WHERE spm.playlist_id = :playlist_id - ORDER BY spm.weight ASC - DQL - )->setParameter('playlist_id', $playlist->getId()) - ->getArrayResult(); - - if ($playlist->getOrder() !== Entity\StationPlaylist::ORDER_SEQUENTIAL) { - shuffle($all_media); + if ($playlist::SOURCE_SONGS !== $playlist->getSource()) { + throw new \InvalidArgumentException('Playlist must contain songs.'); } - $media_queue = []; - foreach ($all_media as $media_row) { - $media_queue[$media_row['id']] = $media_row; + $queuedMediaQuery = $this->em->createQueryBuilder() + ->select(['spm.id AS spm_id', 'sm.id', 'sm.song_id', 'sm.artist', 'sm.title']) + ->from(Entity\StationMedia::class, 'sm') + ->join('sm.playlists', 'spm') + ->where('spm.playlist = :playlist') + ->setParameter('playlist', $playlist); + + if ($playlist::ORDER_RANDOM === $playlist->getOrder()) { + $queuedMediaQuery = $queuedMediaQuery->orderBy('RAND()'); + } else { + $queuedMediaQuery = $queuedMediaQuery->andWhere('spm.is_queued = 1') + ->orderBy('spm.weight', 'ASC'); } - return $media_queue; + $queuedMedia = $queuedMediaQuery->getQuery()->getArrayResult(); + + return array_map( + function ($val): Entity\Api\StationPlaylistQueue { + $record = new Entity\Api\StationPlaylistQueue(); + $record->spm_id = $val['spm_id']; + $record->media_id = $val['id']; + $record->song_id = $val['song_id']; + $record->artist = $val['artist'] ?? ''; + $record->title = $val['title'] ?? ''; + + return $record; + }, + $queuedMedia + ); } } diff --git a/src/Entity/Repository/StationRequestRepository.php b/src/Entity/Repository/StationRequestRepository.php index a92c8a386c..7c3a22823f 100644 --- a/src/Entity/Repository/StationRequestRepository.php +++ b/src/Entity/Repository/StationRequestRepository.php @@ -208,15 +208,13 @@ public function checkRecentPlay(Entity\StationMedia $media, Entity\Station $stat ->setParameter('threshold', $lastPlayThreshold) ->getArrayResult(); + $eligibleTrack = new Entity\Api\StationPlaylistQueue(); + $eligibleTrack->media_id = $media->getId(); + $eligibleTrack->song_id = $media->getSongId(); + $eligibleTrack->title = $media->getTitle() ?? ''; + $eligibleTrack->artist = $media->getArtist() ?? ''; - $eligibleTracks = [ - $media->getId() => [ - 'title' => $media->getTitle(), - 'artist' => $media->getArtist(), - ], - ]; - - $isDuplicate = (null === AutoDJ\Queue::getDistinctTrack($eligibleTracks, $recentTracks)); + $isDuplicate = (null === AutoDJ\Queue::getDistinctTrack([$eligibleTrack], $recentTracks)); if ($isDuplicate) { throw new Exception( diff --git a/src/Entity/StationMedia.php b/src/Entity/StationMedia.php index 1ced7bc47f..6583a10df7 100644 --- a/src/Entity/StationMedia.php +++ b/src/Entity/StationMedia.php @@ -460,22 +460,6 @@ public function setArtUpdatedAt(int $art_updated_at): void $this->art_updated_at = $art_updated_at; } - public function getItemForPlaylist(StationPlaylist $playlist): ?StationPlaylistMedia - { - $item = $this->playlists->filter( - function ($spm) use ($playlist) { - /** @var StationPlaylistMedia $spm */ - return $spm->getPlaylist()->getId() === $playlist->getId(); - } - ); - - $firstItem = $item->first(); - - return ($firstItem instanceof StationPlaylistMedia) - ? $firstItem - : null; - } - public function getCustomFields(): Collection { return $this->custom_fields; diff --git a/src/Entity/StationPlaylist.php b/src/Entity/StationPlaylist.php index b650d12d8e..e092f82171 100644 --- a/src/Entity/StationPlaylist.php +++ b/src/Entity/StationPlaylist.php @@ -250,14 +250,6 @@ class StationPlaylist */ protected $played_at = 0; - /** - * @ORM\Column(name="queue", type="array", nullable=true) - * @AuditLog\AuditIgnore - * - * @var array|null The current queue of unplayed songs for this playlist. - */ - protected $queue; - /** * @ORM\OneToMany(targetEntity="StationPlaylistMedia", mappedBy="playlist", fetch="EXTRA_LAZY") * @ORM\OrderBy({"weight" = "ASC"}) @@ -337,11 +329,6 @@ public function getSource(): string public function setSource(string $source): void { - // Reset the playback queue if source is changed. - if ($source !== $this->source) { - $this->queue = null; - } - $this->source = $source; } @@ -352,11 +339,6 @@ public function getOrder(): string public function setOrder(string $order): void { - // Reset the playback queue if order is changed. - if ($order !== $this->order) { - $this->queue = null; - } - $this->order = $order; } @@ -482,55 +464,6 @@ public function setPlayedAt(int $played_at): void $this->played_at = $played_at; } - /** - * @return mixed[]|null - */ - public function getQueue(): ?array - { - if (null === $this->queue) { - return null; - } - - // Ensure queue is always formatted correctly. - $newQueue = []; - foreach ($this->queue as $media) { - $newQueue[$media['id']] = $media; - } - return $newQueue; - } - - public function setQueue(?array $queue): void - { - $this->queue = $queue; - } - - public function removeFromQueue(StationMedia $media): void - { - $queue = $this->getQueue(); - - if (null !== $queue) { - unset($queue[$media->getId()]); - $this->queue = $queue; - } - } - - public function addToQueue(StationMedia $media): void - { - $queue = $this->getQueue(); - if (null === $queue) { - return; - } - - $queue[$media->getId()] = [ - 'id' => $media->getId(), - 'song_id' => $media->getSongId(), - 'artist' => $media->getArtist(), - 'title' => $media->getTitle(), - ]; - - $this->setQueue($queue); - } - /** * @return Collection|StationPlaylistMedia[] */ diff --git a/src/Entity/StationPlaylistMedia.php b/src/Entity/StationPlaylistMedia.php index 27d0b96bc6..110a89e6d1 100644 --- a/src/Entity/StationPlaylistMedia.php +++ b/src/Entity/StationPlaylistMedia.php @@ -57,6 +57,13 @@ class StationPlaylistMedia implements JsonSerializable */ protected $weight; + /** + * @ORM\Column(name="is_queued", type="boolean") + * + * @var bool + */ + protected $is_queued = true; + /** * @ORM\Column(name="last_played", type="integer") * @var int @@ -104,6 +111,7 @@ public function getLastPlayed(): int public function played(int $timestamp = null): void { $this->last_played = $timestamp ?? time(); + $this->is_queued = false; } /** diff --git a/src/Radio/AutoDJ/Queue.php b/src/Radio/AutoDJ/Queue.php index fdab25d7f5..e5ff15f8a4 100644 --- a/src/Radio/AutoDJ/Queue.php +++ b/src/Radio/AutoDJ/Queue.php @@ -8,6 +8,7 @@ use Carbon\CarbonInterface; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; +use Psr\SimpleCache\CacheInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Queue implements EventSubscriberInterface @@ -37,10 +38,13 @@ class Queue implements EventSubscriberInterface protected Entity\Repository\SongHistoryRepository $historyRepo; + protected CacheInterface $cache; + public function __construct( EntityManagerInterface $em, LoggerInterface $logger, Scheduler $scheduler, + CacheInterface $cache, Entity\Repository\StationPlaylistMediaRepository $spmRepo, Entity\Repository\StationRequestRepository $requestRepo, Entity\Repository\StationQueueRepository $queueRepo, @@ -49,6 +53,8 @@ public function __construct( $this->em = $em; $this->logger = $logger; $this->scheduler = $scheduler; + $this->cache = $cache; + $this->spmRepo = $spmRepo; $this->requestRepo = $requestRepo; $this->queueRepo = $queueRepo; @@ -291,75 +297,13 @@ protected function playSongFromPlaylist( CarbonInterface $now, bool $allowDuplicates = false ): ?Entity\StationQueue { - $mediaToPlay = $this->getQueuedSong($playlist, $recentSongHistory, $allowDuplicates); - - if ($mediaToPlay instanceof Entity\StationMedia) { - $playlist->setPlayedAt($now->getTimestamp()); - $this->em->persist($playlist); - - $spm = $mediaToPlay->getItemForPlaylist($playlist); - if ($spm instanceof Entity\StationPlaylistMedia) { - $spm->played($now->getTimestamp()); - $this->em->persist($spm); - } - - $stationQueueEntry = Entity\StationQueue::fromMedia($playlist->getStation(), $mediaToPlay); - $stationQueueEntry->setPlaylist($playlist); - - $stationQueueEntry->setTimestampCued($now->getTimestamp()); - - $this->em->persist($stationQueueEntry); - $this->em->flush(); - - return $stationQueueEntry; - } - - if (is_array($mediaToPlay)) { - [$mediaUri, $mediaDuration] = $mediaToPlay; - - $playlist->setPlayedAt($now->getTimestamp()); - $this->em->persist($playlist); - - $stationQueueEntry = new Entity\StationQueue( - $playlist->getStation(), - Entity\Song::createFromText('Remote Playlist URL') - ); - - $stationQueueEntry->setPlaylist($playlist); - $stationQueueEntry->setAutodjCustomUri($mediaUri); - $stationQueueEntry->setDuration($mediaDuration); - $stationQueueEntry->setTimestampCued($now->getTimestamp()); - - $this->em->persist($stationQueueEntry); - $this->em->flush(); - - return $stationQueueEntry; - } - - return null; - } - - /** - * @param Entity\StationPlaylist $playlist - * @param array $recentSongHistory - * @param bool $allowDuplicates Whether to return a media ID even if duplicates can't be prevented. - * - * @return Entity\StationMedia|mixed[]|null - */ - protected function getQueuedSong( - Entity\StationPlaylist $playlist, - array $recentSongHistory, - bool $allowDuplicates = false - ) { if (Entity\StationPlaylist::SOURCE_REMOTE_URL === $playlist->getSource()) { - return $this->getMediaFromRemoteUrl($playlist); + return $this->getSongFromRemotePlaylist($playlist, $now); } - $mediaId = null; - switch ($playlist->getOrder()) { case Entity\StationPlaylist::ORDER_RANDOM: - $mediaId = $this->getRandomMediaIdFromPlaylist( + $validTrack = $this->getRandomMediaIdFromPlaylist( $playlist, $recentSongHistory, $allowDuplicates @@ -367,12 +311,12 @@ protected function getQueuedSong( break; case Entity\StationPlaylist::ORDER_SEQUENTIAL: - $mediaId = $this->getSequentialMediaIdFromPlaylist($playlist); + $validTrack = $this->getSequentialMediaIdFromPlaylist($playlist); break; case Entity\StationPlaylist::ORDER_SHUFFLE: default: - $mediaId = $this->getShuffledMediaIdFromPlaylist( + $validTrack = $this->getShuffledMediaIdFromPlaylist( $playlist, $recentSongHistory, $allowDuplicates @@ -380,9 +324,7 @@ protected function getQueuedSong( break; } - $this->em->flush(); - - if (!$mediaId) { + if (null === $validTrack) { $this->logger->warning( sprintf('Playlist "%s" did not return a playable track.', $playlist->getName()), [ @@ -394,7 +336,59 @@ protected function getQueuedSong( return null; } - return $this->em->find(Entity\StationMedia::class, $mediaId); + $mediaToPlay = $this->em->find(Entity\StationMedia::class, $validTrack->media_id); + if (!$mediaToPlay instanceof Entity\StationMedia) { + return null; + } + + $spm = $this->em->find(Entity\StationPlaylistMedia::class, $validTrack->spm_id); + if ($spm instanceof Entity\StationPlaylistMedia) { + $spm->played($now->getTimestamp()); + $this->em->persist($spm); + } + + $playlist->setPlayedAt($now->getTimestamp()); + $this->em->persist($playlist); + + $stationQueueEntry = Entity\StationQueue::fromMedia($playlist->getStation(), $mediaToPlay); + $stationQueueEntry->setPlaylist($playlist); + $stationQueueEntry->setTimestampCued($now->getTimestamp()); + + $this->em->persist($stationQueueEntry); + $this->em->flush(); + + return $stationQueueEntry; + } + + protected function getSongFromRemotePlaylist( + Entity\StationPlaylist $playlist, + CarbonInterface $now + ): ?Entity\StationQueue { + $mediaToPlay = $this->getMediaFromRemoteUrl($playlist); + + if (is_array($mediaToPlay)) { + [$mediaUri, $mediaDuration] = $mediaToPlay; + + $playlist->setPlayedAt($now->getTimestamp()); + $this->em->persist($playlist); + + $stationQueueEntry = new Entity\StationQueue( + $playlist->getStation(), + Entity\Song::createFromText('Remote Playlist URL') + ); + + $stationQueueEntry->setPlaylist($playlist); + $stationQueueEntry->setAutodjCustomUri($mediaUri); + $stationQueueEntry->setDuration($mediaDuration); + $stationQueueEntry->setTimestampCued($now->getTimestamp()); + + $this->em->persist($stationQueueEntry); + $this->em->flush(); + + return $stationQueueEntry; + } + + return null; } /** @@ -415,23 +409,21 @@ protected function getMediaFromRemoteUrl(Entity\StationPlaylist $playlist): ?arr } // Handle a remote playlist containing songs or streams. - $mediaQueue = $playlist->getQueue(); + $queueCacheKey = 'playlist_queue.' . $playlist->getId(); + $mediaQueue = $this->cache->get($queueCacheKey, null); if (empty($mediaQueue)) { $playlistRaw = file_get_contents($playlist->getRemoteUrl()); $mediaQueue = PlaylistParser::getSongs($playlistRaw); } $mediaId = null; - if (!empty($mediaQueue)) { $mediaId = array_shift($mediaQueue); } // Save the modified cache, sans the now-missing entry. - $playlist->setQueue($mediaQueue); - $this->em->persist($playlist); - $this->em->flush(); + $this->cache->set($queueCacheKey, $mediaQueue, 6000); return ($mediaId) ? [$mediaId, 0] @@ -442,45 +434,35 @@ protected function getRandomMediaIdFromPlaylist( Entity\StationPlaylist $playlist, array $recentSongHistory, bool $allowDuplicates - ): ?int { - $mediaQueue = $this->spmRepo->getPlayableMedia($playlist); + ): ?Entity\Api\StationPlaylistQueue { + $mediaQueue = $this->spmRepo->getQueue($playlist); if ($playlist->getAvoidDuplicates()) { return $this->preventDuplicates($mediaQueue, $recentSongHistory, $allowDuplicates); } - $mediaId = array_key_first($mediaQueue); - - return $mediaId; + return array_shift($mediaQueue); } - protected function getSequentialMediaIdFromPlaylist(Entity\StationPlaylist $playlist): ?int - { - $mediaQueue = $playlist->getQueue(); - + protected function getSequentialMediaIdFromPlaylist( + Entity\StationPlaylist $playlist + ): ?Entity\Api\StationPlaylistQueue { + $mediaQueue = $this->spmRepo->getQueue($playlist); if (empty($mediaQueue)) { - $mediaQueue = $this->spmRepo->getPlayableMedia($playlist); + $mediaQueue = $this->spmRepo->resetQueue($playlist); } - $nextMediaArray = array_shift($mediaQueue); - $mediaId = $nextMediaArray['id']; - - $playlist->setQueue($mediaQueue); - $this->em->persist($playlist); - - return $mediaId; + return array_shift($mediaQueue); } protected function getShuffledMediaIdFromPlaylist( Entity\StationPlaylist $playlist, array $recentSongHistory, bool $allowDuplicates - ): ?int { - $mediaId = null; - $mediaQueue = $playlist->getQueue(); - + ): ?Entity\Api\StationPlaylistQueue { + $mediaQueue = $this->spmRepo->getQueue($playlist); if (empty($mediaQueue)) { - $mediaQueue = $this->spmRepo->getPlayableMedia($playlist); + $mediaQueue = $this->spmRepo->resetQueue($playlist); } if ($playlist->getAvoidDuplicates()) { @@ -489,27 +471,17 @@ protected function getShuffledMediaIdFromPlaylist( 'Duplicate prevention yielded no playable song; resetting song queue.' ); - $mediaQueue = $this->spmRepo->getPlayableMedia($playlist); + $mediaQueue = $this->spmRepo->resetQueue($playlist); } - $mediaId = $this->preventDuplicates($mediaQueue, $recentSongHistory, $allowDuplicates); - } else { - $mediaId = array_key_first($mediaQueue); - } - - if (null !== $mediaId) { - unset($mediaQueue[$mediaId]); + return $this->preventDuplicates($mediaQueue, $recentSongHistory, $allowDuplicates); } - // Save the modified cache, sans the now-missing entry. - $playlist->setQueue($mediaQueue); - $this->em->persist($playlist); - - return $mediaId; + return array_shift($mediaQueue); } /** - * @param array $eligibleTracks + * @param Entity\Api\StationPlaylistQueue[] $eligibleTracks * @param array $playedTracks * @param bool $allowDuplicates Whether to return a media ID even if duplicates can't be prevented. */ @@ -517,7 +489,7 @@ protected function preventDuplicates( array $eligibleTracks = [], array $playedTracks = [], bool $allowDuplicates = false - ): ?int { + ): ?Entity\Api\StationPlaylistQueue { if (empty($eligibleTracks)) { $this->logger->debug('Eligible song queue is empty!'); return null; @@ -533,10 +505,11 @@ protected function preventDuplicates( } } + /** @var Entity\Api\StationPlaylistQueue[] $notPlayedEligibleTracks */ $notPlayedEligibleTracks = []; foreach ($eligibleTracks as $mediaId => $track) { - $songId = $track['song_id']; + $songId = $track->song_id; if (isset($latestSongIdsPlayed[$songId])) { continue; } @@ -544,38 +517,49 @@ protected function preventDuplicates( $notPlayedEligibleTracks[$mediaId] = $track; } - $mediaId = self::getDistinctTrack($notPlayedEligibleTracks, $playedTracks); + $validTrack = self::getDistinctTrack($notPlayedEligibleTracks, $playedTracks); - if (null !== $mediaId) { + if (null !== $validTrack) { $this->logger->info( 'Found track that avoids duplicate title and artist.', - ['media_id' => $mediaId] + [ + 'media_id' => $validTrack->media_id, + 'title' => $validTrack->title, + 'artist' => $validTrack->artist, + ] ); - return $mediaId; + return $validTrack; } + // If we reach this point, there's no way to avoid a duplicate title and artist. if ($allowDuplicates) { - // If we reach this point, there's no way to avoid a duplicate title. + /** @var Entity\Api\StationPlaylistQueue[] $mediaIdsByTimePlayed */ $mediaIdsByTimePlayed = []; // For each piece of eligible media, get its latest played timestamp. foreach ($eligibleTracks as $track) { - $songId = $track['song_id']; - $mediaIdsByTimePlayed[$track['id']] = $latestSongIdsPlayed[$songId] ?? 0; + $songId = $track->song_id; + $trackKey = $latestSongIdsPlayed[$songId] ?? 0; + $mediaIdsByTimePlayed[$trackKey] = $track; } - // Pull the lowest value, which corresponds to the least recently played song. - asort($mediaIdsByTimePlayed); + ksort($mediaIdsByTimePlayed); - $mediaId = array_key_first($mediaIdsByTimePlayed); - if (null !== $mediaId) { + $validTrack = array_shift($mediaIdsByTimePlayed); + + // Pull the lowest value, which corresponds to the least recently played song. + if (null !== $validTrack) { $this->logger->warning( 'No way to avoid same title OR same artist; using least recently played song.', - ['media_id' => $mediaId] + [ + 'media_id' => $validTrack->media_id, + 'title' => $validTrack->title, + 'artist' => $validTrack->artist, + ] ); - return $mediaId; + return $validTrack; } } @@ -615,13 +599,14 @@ public function getNextSongFromRequests(BuildQueue $event): void * Both should be in the form of an array, i.e.: * [ 'id' => ['artist' => 'Foo', 'title' => 'Fighters'] ] * - * @param array $eligibleTracks + * @param Entity\Api\StationPlaylistQueue[] $eligibleTracks * @param array $playedTracks * - * @return int|string|null */ - public static function getDistinctTrack(array $eligibleTracks, array $playedTracks) - { + public static function getDistinctTrack( + array $eligibleTracks, + array $playedTracks + ): ?Entity\Api\StationPlaylistQueue { $artistSeparators = [ ', ', ' feat ', @@ -657,18 +642,19 @@ public static function getDistinctTrack(array $eligibleTracks, array $playedTrac } } + /** @var Entity\Api\StationPlaylistQueue[] $eligibleTracksWithoutSameTitle */ $eligibleTracksWithoutSameTitle = []; - foreach ($eligibleTracks as $mediaId => $track) { + foreach ($eligibleTracks as $track) { // Avoid all direct title matches. - $title = trim($track['title']); + $title = trim($track->title); if (isset($titles[$title])) { continue; } // Attempt to avoid an artist match, if possible. - $artist = trim($track['artist']); + $artist = trim($track->artist); $artistMatchFound = false; if (!empty($artist)) { @@ -687,23 +673,16 @@ public static function getDistinctTrack(array $eligibleTracks, array $playedTrac } if (!$artistMatchFound) { - return $mediaId; + return $track; } - $eligibleTracksWithoutSameTitle[$mediaId] = $track; + $songId = $track->song_id; + $trackKey = $latestSongIdsPlayed[$songId] ?? 0; + $eligibleTracksWithoutSameTitle[$trackKey] = $track; } - $mediaIdsByTimePlayed = []; - - foreach ($eligibleTracksWithoutSameTitle as $mediaId => $track) { - $songId = $track['song_id']; - - $mediaIdsByTimePlayed[$mediaId] = $latestSongIdsPlayed[$songId] ?? 0; - } - - asort($mediaIdsByTimePlayed); - - return array_key_first($mediaIdsByTimePlayed); + ksort($eligibleTracksWithoutSameTitle); + return array_shift($eligibleTracksWithoutSameTitle); } protected function logRecentSongHistory(