Update Queue to be based on expected play time, not expected cue time.
This commit is contained in:
parent
ba56e055b0
commit
ccf1e5487f
|
@ -35,8 +35,9 @@
|
|||
{{ row.item.song.text }}
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(cued_at)="row">
|
||||
{{ formatTime(row.item.cued_at) }}
|
||||
<template #cell(played_at)="row">
|
||||
{{ formatTime(row.item.played_at) }}<br>
|
||||
<small>{{ formatRelativeTime(row.item.played_at) }}</small>
|
||||
</template>
|
||||
<template #cell(source)="row">
|
||||
<div v-if="row.item.is_request">
|
||||
|
@ -73,19 +74,25 @@ export default {
|
|||
fields: [
|
||||
{key: 'actions', label: this.$gettext('Actions'), sortable: false},
|
||||
{key: 'song_title', isRowHeader: true, label: this.$gettext('Song Title'), sortable: false},
|
||||
{key: 'cued_at', label: this.$gettext('Cued On'), sortable: false},
|
||||
{key: 'played_at', label: this.$gettext('Expected to Play at'), sortable: false},
|
||||
{key: 'source', label: this.$gettext('Source'), sortable: false}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formatTime (time) {
|
||||
return DateTime.fromSeconds(time).setZone(this.stationTimeZone).toLocaleString(DateTime.DATETIME_MED);
|
||||
formatTime(time) {
|
||||
return this.getDateTime(time).toLocaleString(DateTime.TIME_WITH_SECONDS);
|
||||
},
|
||||
doShowLogs (logs) {
|
||||
formatRelativeTime(time) {
|
||||
return this.getDateTime(time).toRelative();
|
||||
},
|
||||
getDateTime(timestamp) {
|
||||
return DateTime.fromSeconds(timestamp).setZone(this.stationTimeZone);
|
||||
},
|
||||
doShowLogs(logs) {
|
||||
this.$refs.logs_modal.show(logs);
|
||||
},
|
||||
doDelete (url) {
|
||||
doDelete(url) {
|
||||
this.$confirmDelete({
|
||||
title: this.$gettext('Delete Queue Item?'),
|
||||
}).then((result) => {
|
||||
|
|
|
@ -15,13 +15,21 @@ use Psr\Http\Message\UriInterface;
|
|||
class StationQueue implements ResolvableUrlInterface
|
||||
{
|
||||
/**
|
||||
* UNIX timestamp when playback is expected to start.
|
||||
* UNIX timestamp when the AutoDJ is expected to queue the song for playback.
|
||||
*
|
||||
* @OA\Property(example=1609480800)
|
||||
* @var int
|
||||
*/
|
||||
public int $cued_at = 0;
|
||||
|
||||
/**
|
||||
* UNIX timestamp when playback is expected to start.
|
||||
*
|
||||
* @OA\Property(example=1609480800)
|
||||
* @var int
|
||||
*/
|
||||
public int $played_at = 0;
|
||||
|
||||
/**
|
||||
* Duration of the song in seconds
|
||||
*
|
||||
|
|
|
@ -23,6 +23,7 @@ class StationQueueApiGenerator
|
|||
): Entity\Api\NowPlaying\StationQueue {
|
||||
$response = new Entity\Api\NowPlaying\StationQueue();
|
||||
$response->cued_at = $record->getTimestampCued();
|
||||
$response->played_at = $record->getTimestampPlayed();
|
||||
$response->duration = (int)$record->getDuration();
|
||||
$response->is_request = $record->getRequest() !== null;
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\Migration;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211124165404 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add "timestamp_played" to queue items.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE station_queue ADD timestamp_played INT NOT NULL');
|
||||
}
|
||||
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$this->connection->executeQuery(
|
||||
'UPDATE station_queue SET timestamp_played=timestamp_cued WHERE timestamp_played IS NULL'
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE station_queue DROP timestamp_played');
|
||||
}
|
||||
}
|
|
@ -67,10 +67,10 @@ class StationQueueRepository extends Repository
|
|||
): array {
|
||||
return $this->em->createQuery(
|
||||
<<<'DQL'
|
||||
SELECT sq.timestamp_cued, sq.playlist_id
|
||||
SELECT sq.timestamp_played, sq.playlist_id
|
||||
FROM App\Entity\StationQueue sq
|
||||
WHERE sq.station = :station
|
||||
ORDER BY sq.timestamp_cued DESC
|
||||
ORDER BY sq.timestamp_played DESC
|
||||
DQL
|
||||
)->setParameter('station', $station)
|
||||
->setMaxResults($rows)
|
||||
|
@ -89,11 +89,11 @@ class StationQueueRepository extends Repository
|
|||
|
||||
return $this->em->createQuery(
|
||||
<<<'DQL'
|
||||
SELECT sq.song_id, sq.timestamp_cued, sq.title, sq.artist
|
||||
SELECT sq.song_id, sq.timestamp_played, sq.title, sq.artist
|
||||
FROM App\Entity\StationQueue sq
|
||||
WHERE sq.station = :station
|
||||
AND sq.timestamp_cued >= :threshold
|
||||
ORDER BY sq.timestamp_cued DESC
|
||||
AND sq.timestamp_played >= :threshold
|
||||
ORDER BY sq.timestamp_played DESC
|
||||
DQL
|
||||
)->setParameter('station', $station)
|
||||
->setParameter('threshold', $threshold)
|
||||
|
|
|
@ -58,6 +58,9 @@ class StationQueue implements SongInterface, IdentifiableEntityInterface
|
|||
#[ORM\Column]
|
||||
protected int $timestamp_cued;
|
||||
|
||||
#[ORM\Column]
|
||||
protected int $timestamp_played;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
protected ?int $duration = null;
|
||||
|
||||
|
@ -161,10 +164,21 @@ class StationQueue implements SongInterface, IdentifiableEntityInterface
|
|||
{
|
||||
if ($newValue) {
|
||||
$this->sent_to_autodj = true;
|
||||
$this->setTimestampPlayed(time());
|
||||
}
|
||||
$this->is_played = $newValue;
|
||||
}
|
||||
|
||||
public function getTimestampPlayed(): int
|
||||
{
|
||||
return $this->timestamp_played;
|
||||
}
|
||||
|
||||
public function setTimestampPlayed(int $timestamp_played): void
|
||||
{
|
||||
$this->timestamp_played = $timestamp_played;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]|null
|
||||
*/
|
||||
|
|
|
@ -11,15 +11,19 @@ use Symfony\Contracts\EventDispatcher\Event;
|
|||
|
||||
class BuildQueue extends Event
|
||||
{
|
||||
protected ?Entity\StationQueue $next_song = null;
|
||||
protected ?Entity\StationQueue $nextSong = null;
|
||||
|
||||
protected CarbonInterface $now;
|
||||
protected CarbonInterface $expectedCueTime;
|
||||
|
||||
protected CarbonInterface $expectedPlayTime;
|
||||
|
||||
public function __construct(
|
||||
protected Entity\Station $station,
|
||||
?CarbonInterface $now = null
|
||||
?CarbonInterface $expectedCueTime = null,
|
||||
?CarbonInterface $expectedPlayTime = null
|
||||
) {
|
||||
$this->now = $now ?? CarbonImmutable::now($station->getTimezoneObject());
|
||||
$this->expectedCueTime = $expectedCueTime ?? CarbonImmutable::now($station->getTimezoneObject());
|
||||
$this->expectedPlayTime = $expectedPlayTime ?? CarbonImmutable::now($station->getTimezoneObject());
|
||||
}
|
||||
|
||||
public function getStation(): Entity\Station
|
||||
|
@ -27,21 +31,26 @@ class BuildQueue extends Event
|
|||
return $this->station;
|
||||
}
|
||||
|
||||
public function getNow(): CarbonInterface
|
||||
public function getExpectedCueTime(): CarbonInterface
|
||||
{
|
||||
return $this->now;
|
||||
return $this->expectedCueTime;
|
||||
}
|
||||
|
||||
public function getExpectedPlayTime(): CarbonInterface
|
||||
{
|
||||
return $this->expectedPlayTime;
|
||||
}
|
||||
|
||||
public function getNextSong(): ?Entity\StationQueue
|
||||
{
|
||||
return $this->next_song;
|
||||
return $this->nextSong;
|
||||
}
|
||||
|
||||
public function setNextSong(?Entity\StationQueue $next_song): bool
|
||||
public function setNextSong(?Entity\StationQueue $nextSong): bool
|
||||
{
|
||||
$this->next_song = $next_song;
|
||||
$this->nextSong = $nextSong;
|
||||
|
||||
if (null !== $next_song) {
|
||||
if (null !== $nextSong) {
|
||||
$this->stopPropagation();
|
||||
return true;
|
||||
}
|
||||
|
@ -51,13 +60,13 @@ class BuildQueue extends Event
|
|||
|
||||
public function hasNextSong(): bool
|
||||
{
|
||||
return (null !== $this->next_song);
|
||||
return (null !== $this->nextSong);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return (null !== $this->next_song)
|
||||
? (string)$this->next_song
|
||||
return (null !== $this->nextSong)
|
||||
? (string)$this->nextSong
|
||||
: 'No Song';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,9 +130,25 @@ class AutoDJ
|
|||
}
|
||||
);
|
||||
|
||||
// Adjust "now" time from current queue.
|
||||
// Adjust "expectedCueTime" time from current queue.
|
||||
$tzObject = $station->getTimezoneObject();
|
||||
$now = CarbonImmutable::now($tzObject);
|
||||
$expectedCueTime = CarbonImmutable::now($tzObject);
|
||||
|
||||
// Get expected play time of each item.
|
||||
$currentSong = $this->songHistoryRepo->getCurrent($station);
|
||||
if (null !== $currentSong) {
|
||||
$expectedPlayTime = $this->addDurationToTime(
|
||||
$station,
|
||||
CarbonImmutable::createFromTimestamp($currentSong->getTimestampStart(), $tzObject),
|
||||
$currentSong->getDuration()
|
||||
);
|
||||
|
||||
if ($expectedPlayTime < $expectedCueTime) {
|
||||
$expectedPlayTime = $expectedCueTime;
|
||||
}
|
||||
} else {
|
||||
$expectedPlayTime = $expectedCueTime;
|
||||
}
|
||||
|
||||
$maxQueueLength = $station->getBackendConfig()->getAutoDjQueueLength();
|
||||
if ($maxQueueLength < 2) {
|
||||
|
@ -155,24 +171,27 @@ class AutoDJ
|
|||
$lastSongId = $queueRow->getSongId();
|
||||
|
||||
if ($queueRow->getSentToAutodj()) {
|
||||
$now = $this->getAdjustedNow(
|
||||
$expectedCueTime = $this->addDurationToTime(
|
||||
$station,
|
||||
CarbonImmutable::createFromTimestamp($queueRow->getTimestampCued(), $tzObject),
|
||||
$queueRow->getDuration()
|
||||
);
|
||||
} else {
|
||||
$queueRow->setTimestampCued($now->getTimestamp());
|
||||
$queueRow->setTimestampCued($expectedCueTime->getTimestamp());
|
||||
$queueRow->setTimestampPlayed($expectedPlayTime->getTimestamp());
|
||||
$this->em->persist($queueRow);
|
||||
|
||||
$now = $this->getAdjustedNow($station, $now, $queueRow->getDuration());
|
||||
$expectedCueTime = $this->addDurationToTime($station, $expectedCueTime, $queueRow->getDuration());
|
||||
}
|
||||
|
||||
$expectedPlayTime = $this->addDurationToTime($station, $expectedPlayTime, $queueRow->getDuration());
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
// Build the remainder of the queue.
|
||||
while ($queueLength < $maxQueueLength) {
|
||||
$queueRow = $this->cueNextSong($station, $now);
|
||||
$queueRow = $this->cueNextSong($station, $expectedCueTime, $expectedPlayTime);
|
||||
if ($queueRow instanceof Entity\StationQueue) {
|
||||
$this->em->persist($queueRow);
|
||||
|
||||
|
@ -181,7 +200,17 @@ class AutoDJ
|
|||
$this->em->remove($queueRow);
|
||||
} else {
|
||||
$lastSongId = $queueRow->getSongId();
|
||||
$now = $this->getAdjustedNow($station, $now, $queueRow->getDuration());
|
||||
|
||||
$expectedCueTime = $this->addDurationToTime(
|
||||
$station,
|
||||
$expectedCueTime,
|
||||
$queueRow->getDuration()
|
||||
);
|
||||
$expectedPlayTime = $this->addDurationToTime(
|
||||
$station,
|
||||
$expectedPlayTime,
|
||||
$queueRow->getDuration()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
|
@ -197,7 +226,7 @@ class AutoDJ
|
|||
}
|
||||
}
|
||||
|
||||
protected function getAdjustedNow(Entity\Station $station, CarbonInterface $now, ?int $duration): CarbonInterface
|
||||
protected function addDurationToTime(Entity\Station $station, CarbonInterface $now, ?int $duration): CarbonInterface
|
||||
{
|
||||
$duration ??= 1;
|
||||
|
||||
|
@ -209,12 +238,15 @@ class AutoDJ
|
|||
: $now;
|
||||
}
|
||||
|
||||
protected function cueNextSong(Entity\Station $station, CarbonInterface $now): ?Entity\StationQueue
|
||||
{
|
||||
protected function cueNextSong(
|
||||
Entity\Station $station,
|
||||
CarbonInterface $expectedCueTime,
|
||||
CarbonInterface $expectedPlayTime
|
||||
): ?Entity\StationQueue {
|
||||
$this->logger->debug(
|
||||
'Adding to station queue.',
|
||||
[
|
||||
'now' => (string)$now,
|
||||
'now' => (string)$expectedPlayTime,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -222,13 +254,16 @@ class AutoDJ
|
|||
$testHandler = new TestHandler($this->environment->getLogLevel(), true);
|
||||
$this->logger->pushHandler($testHandler);
|
||||
|
||||
$event = new BuildQueue($station, $now);
|
||||
$event = new BuildQueue($station, $expectedCueTime, $expectedPlayTime);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$this->logger->popHandler();
|
||||
|
||||
$queueRow = $event->getNextSong();
|
||||
if ($queueRow instanceof Entity\StationQueue) {
|
||||
$queueRow->setTimestampCued($expectedCueTime->getTimestamp());
|
||||
$queueRow->setTimestampPlayed($expectedPlayTime->getTimestamp());
|
||||
|
||||
$queueRow->setLog($testHandler->getRecords());
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ class Queue implements EventSubscriberInterface
|
|||
$this->logger->info('AzuraCast AutoDJ is calculating the next song to play...');
|
||||
|
||||
$station = $event->getStation();
|
||||
$now = $event->getNow();
|
||||
$expectedPlayTime = $event->getExpectedPlayTime();
|
||||
|
||||
[$activePlaylistsByType, $oncePerXSongHistoryCount] = $this->getActivePlaylistsSortedByType($station);
|
||||
|
||||
|
@ -77,12 +77,11 @@ class Queue implements EventSubscriberInterface
|
|||
|
||||
$recentSongHistoryForDuplicatePrevention = $this->queueRepo->getRecentlyPlayedByTimeRange(
|
||||
$station,
|
||||
$now,
|
||||
$expectedPlayTime,
|
||||
$station->getBackendConfig()->getDuplicatePreventionTimeRange()
|
||||
);
|
||||
|
||||
$this->logRecentSongHistory(
|
||||
$now,
|
||||
$recentPlaylistHistory,
|
||||
$recentSongHistoryForDuplicatePrevention
|
||||
);
|
||||
|
@ -95,7 +94,7 @@ class Queue implements EventSubscriberInterface
|
|||
[$eligiblePlaylists, $logPlaylists] = $this->filterEligiblePlaylists(
|
||||
$activePlaylistsByType,
|
||||
$currentPlaylistType,
|
||||
$now,
|
||||
$expectedPlayTime,
|
||||
$recentPlaylistHistory
|
||||
);
|
||||
|
||||
|
@ -119,7 +118,7 @@ class Queue implements EventSubscriberInterface
|
|||
$eligiblePlaylists,
|
||||
$activePlaylistsByType,
|
||||
$recentSongHistoryForDuplicatePrevention,
|
||||
$now,
|
||||
$expectedPlayTime,
|
||||
$allowDuplicates
|
||||
);
|
||||
|
||||
|
@ -177,7 +176,7 @@ class Queue implements EventSubscriberInterface
|
|||
protected function filterEligiblePlaylists(
|
||||
array $playlistsByType,
|
||||
string $type,
|
||||
CarbonInterface $now,
|
||||
CarbonInterface $expectedPlayTime,
|
||||
array $recentPlaylistHistory
|
||||
): array {
|
||||
$eligiblePlaylists = [];
|
||||
|
@ -185,7 +184,7 @@ class Queue implements EventSubscriberInterface
|
|||
|
||||
foreach ($playlistsByType[$type] as $playlistId => $playlist) {
|
||||
/** @var Entity\StationPlaylist $playlist */
|
||||
if (!$this->scheduler->shouldPlaylistPlayNow($playlist, $now, $recentPlaylistHistory)) {
|
||||
if (!$this->scheduler->shouldPlaylistPlayNow($playlist, $expectedPlayTime, $recentPlaylistHistory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -239,7 +238,7 @@ class Queue implements EventSubscriberInterface
|
|||
array $eligiblePlaylists,
|
||||
array $activePlaylistsByType,
|
||||
array $recentSongHistoryForDuplicatePrevention,
|
||||
CarbonInterface $now,
|
||||
CarbonInterface $expectedPlayTime,
|
||||
bool $allowDuplicates
|
||||
): ?Entity\StationQueue {
|
||||
foreach ($eligiblePlaylists as $playlistId => $weight) {
|
||||
|
@ -248,7 +247,7 @@ class Queue implements EventSubscriberInterface
|
|||
$nextSong = $this->playSongFromPlaylist(
|
||||
$playlist,
|
||||
$recentSongHistoryForDuplicatePrevention,
|
||||
$now,
|
||||
$expectedPlayTime,
|
||||
$allowDuplicates
|
||||
);
|
||||
|
||||
|
@ -265,17 +264,17 @@ class Queue implements EventSubscriberInterface
|
|||
*
|
||||
* @param Entity\StationPlaylist $playlist
|
||||
* @param array $recentSongHistory
|
||||
* @param CarbonInterface $now
|
||||
* @param CarbonInterface $expectedPlayTime
|
||||
* @param bool $allowDuplicates Whether to return a media ID even if duplicates can't be prevented.
|
||||
*/
|
||||
protected function playSongFromPlaylist(
|
||||
Entity\StationPlaylist $playlist,
|
||||
array $recentSongHistory,
|
||||
CarbonInterface $now,
|
||||
CarbonInterface $expectedPlayTime,
|
||||
bool $allowDuplicates = false
|
||||
): ?Entity\StationQueue {
|
||||
if (Entity\StationPlaylist::SOURCE_REMOTE_URL === $playlist->getSource()) {
|
||||
return $this->getSongFromRemotePlaylist($playlist, $now);
|
||||
return $this->getSongFromRemotePlaylist($playlist, $expectedPlayTime);
|
||||
}
|
||||
|
||||
$validTrack = match ($playlist->getOrder()) {
|
||||
|
@ -312,33 +311,30 @@ class Queue implements EventSubscriberInterface
|
|||
|
||||
$spm = $this->em->find(Entity\StationPlaylistMedia::class, $validTrack->spm_id);
|
||||
if ($spm instanceof Entity\StationPlaylistMedia) {
|
||||
$spm->played($now->getTimestamp());
|
||||
$spm->played($expectedPlayTime->getTimestamp());
|
||||
$this->em->persist($spm);
|
||||
}
|
||||
|
||||
$playlist->setPlayedAt($now->getTimestamp());
|
||||
$playlist->setPlayedAt($expectedPlayTime->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
|
||||
CarbonInterface $expectedPlayTime
|
||||
): ?Entity\StationQueue {
|
||||
$mediaToPlay = $this->getMediaFromRemoteUrl($playlist);
|
||||
|
||||
if (is_array($mediaToPlay)) {
|
||||
[$mediaUri, $mediaDuration] = $mediaToPlay;
|
||||
|
||||
$playlist->setPlayedAt($now->getTimestamp());
|
||||
$playlist->setPlayedAt($expectedPlayTime->getTimestamp());
|
||||
$this->em->persist($playlist);
|
||||
|
||||
$stationQueueEntry = new Entity\StationQueue(
|
||||
|
@ -349,10 +345,8 @@ class Queue implements EventSubscriberInterface
|
|||
$stationQueueEntry->setPlaylist($playlist);
|
||||
$stationQueueEntry->setAutodjCustomUri($mediaUri);
|
||||
$stationQueueEntry->setDuration($mediaDuration);
|
||||
$stationQueueEntry->setTimestampCued($now->getTimestamp());
|
||||
|
||||
$this->em->persist($stationQueueEntry);
|
||||
$this->em->flush();
|
||||
|
||||
return $stationQueueEntry;
|
||||
}
|
||||
|
@ -547,9 +541,9 @@ class Queue implements EventSubscriberInterface
|
|||
*/
|
||||
public function getNextSongFromRequests(BuildQueue $event): void
|
||||
{
|
||||
$now = $event->getNow();
|
||||
$expectedPlayTime = $event->getExpectedPlayTime();
|
||||
|
||||
$request = $this->requestRepo->getNextPlayableRequest($event->getStation(), $now);
|
||||
$request = $this->requestRepo->getNextPlayableRequest($event->getStation(), $expectedPlayTime);
|
||||
if (null === $request) {
|
||||
return;
|
||||
}
|
||||
|
@ -557,14 +551,11 @@ class Queue implements EventSubscriberInterface
|
|||
$this->logger->debug(sprintf('Queueing next song from request ID %d.', $request->getId()));
|
||||
|
||||
$stationQueueEntry = Entity\StationQueue::fromRequest($request);
|
||||
$stationQueueEntry->setTimestampCued($now->getTimestamp());
|
||||
$this->em->persist($stationQueueEntry);
|
||||
|
||||
$request->setPlayedAt($now->getTimestamp());
|
||||
$request->setPlayedAt($expectedPlayTime->getTimestamp());
|
||||
$this->em->persist($request);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$event->setNextSong($stationQueueEntry);
|
||||
}
|
||||
|
||||
|
@ -662,7 +653,6 @@ class Queue implements EventSubscriberInterface
|
|||
}
|
||||
|
||||
protected function logRecentSongHistory(
|
||||
CarbonInterface $now,
|
||||
array $recentPlaylistHistory,
|
||||
array $recentSongHistoryForDuplicatePrevention
|
||||
): void {
|
||||
|
|
Loading…
Reference in New Issue