Update Queue to be based on expected play time, not expected cue time.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-11-24 11:59:16 -06:00
parent ba56e055b0
commit ccf1e5487f
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
9 changed files with 163 additions and 66 deletions

View File

@ -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) => {

View File

@ -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
*

View File

@ -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;

View File

@ -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');
}
}

View File

@ -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)

View File

@ -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
*/

View File

@ -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';
}
}

View File

@ -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());
}

View File

@ -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 {