4
0
mirror of https://github.com/AzuraCast/AzuraCast.git synced 2024-06-17 14:37:07 +00:00

Merge commit '8bc9cb70f77b73c3305ebf8d0d60f13822ebcba0'

This commit is contained in:
Buster "Silver Eagle" Neece 2022-06-11 10:30:47 -05:00
parent a3b0ee5ce6
commit c90d5bdf50
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
8 changed files with 215 additions and 181 deletions

View File

@ -6,6 +6,7 @@ namespace App\Entity\ApiGenerator;
use App\Entity;
use App\Http\Router;
use App\Radio\Enums\BackendAdapters;
use GuzzleHttp\Psr7\Uri;
use NowPlaying\Result\CurrentSong;
use NowPlaying\Result\Result;
@ -47,38 +48,26 @@ class NowPlayingApiGenerator
unique: $npResult->listeners->unique
);
// Pull from current NP data if song details haven't changed .
if (
$npOld instanceof Entity\Api\NowPlaying\NowPlaying
&& $this->tracksMatch($npResult->currentSong, $npOld->now_playing?->song->id)
) {
$previousHistory = $this->historyRepo->getCurrent($station);
$updateSongFromNowPlaying = (BackendAdapters::Liquidsoap !== $station->getBackendTypeEnum());
if (null === $previousHistory) {
$previousHistory = ($npOld->now_playing?->song)
? Entity\Song::createFromApiSong($npOld->now_playing->song)
: Entity\Song::createOffline();
}
$sh_obj = $this->historyRepo->register($previousHistory, $station, $np);
$np->song_history = $npOld->song_history;
} else {
// SongHistory registration must ALWAYS come before the history/nextsong calls
// otherwise they will not have up-to-date database info!
$sh_obj = $this->historyRepo->register(
Entity\Song::createFromNowPlayingSong($npResult->currentSong),
try {
$sh_obj = $this->historyRepo->updateFromNowPlaying(
$station,
$np
);
$np->song_history = $this->songHistoryApiGenerator->fromArray(
$this->historyRepo->getVisibleHistory($station),
$baseUri,
true
$np->listeners->current,
($updateSongFromNowPlaying)
? Entity\Song::createFromNowPlayingSong($npResult->currentSong)
: null
);
} catch (\Exception) {
return $this->offlineApi($station, $baseUri);
}
$np->song_history = $this->songHistoryApiGenerator->fromArray(
$this->historyRepo->getVisibleHistory($station),
$baseUri,
true
);
$nextVisibleSong = $this->queueRepo->getNextVisible($station);
if (null === $nextVisibleSong) {
$np->playing_next = $npOld->playing_next ?? null;

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Entity\Migration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220611123923 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add current_song relation to Station.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE station ADD current_song_id INT DEFAULT NULL');
$this->addSql(
'ALTER TABLE station ADD CONSTRAINT FK_9F39F8B1AB03776 FOREIGN KEY (current_song_id) REFERENCES song_history (id) ON DELETE SET NULL'
);
$this->addSql('CREATE INDEX IDX_9F39F8B1AB03776 ON station (current_song_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE station DROP FOREIGN KEY FK_9F39F8B1AB03776');
$this->addSql('DROP INDEX IDX_9F39F8B1AB03776 ON station');
$this->addSql('ALTER TABLE station DROP current_song_id');
}
}

View File

@ -6,8 +6,6 @@ namespace App\Entity\Repository;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Radio\Backend\Liquidsoap\Command\FeedbackCommand;
use App\Radio\Enums\BackendAdapters;
use Carbon\CarbonImmutable;
/**
@ -18,8 +16,7 @@ final class SongHistoryRepository extends AbstractStationBasedRepository
public function __construct(
ReloadableEntityManagerInterface $em,
private readonly ListenerRepository $listenerRepository,
private readonly StationQueueRepository $stationQueueRepository,
private readonly FeedbackCommand $liquidsoapFeedback,
private readonly StationQueueRepository $stationQueueRepository
) {
parent::__construct($em);
}
@ -58,123 +55,74 @@ final class SongHistoryRepository extends AbstractStationBasedRepository
return $records;
}
public function register(
Entity\Interfaces\SongInterface $song,
public function updateFromNowPlaying(
Entity\Station $station,
Entity\Api\NowPlaying\NowPlaying $np
int $listeners,
?Entity\Interfaces\SongInterface $song = null,
): Entity\SongHistory {
// Pull the most recent history item for this station.
$last_sh = $this->getCurrent($station);
$currentSong = $station->getCurrentSong();
$listeners = $np->listeners->current;
if ($last_sh instanceof Entity\SongHistory) {
if ($last_sh->getSongId() === $song->getSongId()) {
// Updating the existing SongHistory item with a new data point.
$last_sh->addDeltaPoint($listeners);
$this->em->persist($last_sh);
$this->em->flush();
return $last_sh;
if (null !== $song && $this->isDifferentFromCurrentSong($station, $song)) {
// Handle track transition.
$upcomingTrack = $this->stationQueueRepository->findRecentlyCuedSong($station, $song);
if (null !== $upcomingTrack) {
$this->stationQueueRepository->trackPlayed($station, $upcomingTrack);
$newSong = Entity\SongHistory::fromQueue($upcomingTrack);
} else {
$newSong = new Entity\SongHistory($station, $song);
}
$currentSong = $this->changeCurrentSong($station, $newSong);
}
if (null === $currentSong) {
throw new \RuntimeException('No track to update.');
}
$currentSong->addDeltaPoint($listeners);
$this->em->persist($currentSong);
$this->em->flush();
return $currentSong;
}
public function isDifferentFromCurrentSong(
Entity\Station $station,
Entity\Interfaces\SongInterface $toCompare
): bool {
$currentSong = $station->getCurrentSong();
return !(null !== $currentSong) || $currentSong->getSongId() !== $toCompare->getSongId();
}
public function changeCurrentSong(
Entity\Station $station,
Entity\SongHistory $newCurrentSong
): Entity\SongHistory {
$previousCurrentSong = $station->getCurrentSong();
if (null !== $previousCurrentSong) {
// Wrapping up processing on the previous SongHistory item (if present).
$last_sh->setTimestampEnd(time());
$last_sh->setListenersEnd($listeners);
$previousCurrentSong->playbackEnded();
// Calculate "delta" data for previous item, based on all data points.
$last_sh->addDeltaPoint($listeners);
$delta_points = (array)$last_sh->getDeltaPoints();
$delta_positive = 0;
$delta_negative = 0;
$delta_total = 0;
for ($i = 1, $iMax = count($delta_points); $i < $iMax; $i++) {
$current_delta = $delta_points[$i];
$previous_delta = $delta_points[$i - 1];
$delta_delta = $current_delta - $previous_delta;
$delta_total += $delta_delta;
if ($delta_delta > 0) {
$delta_positive += $delta_delta;
} elseif ($delta_delta < 0) {
$delta_negative += (int)abs($delta_delta);
}
}
$last_sh->setDeltaPositive((int)$delta_positive);
$last_sh->setDeltaNegative((int)$delta_negative);
$last_sh->setDeltaTotal((int)$delta_total);
$last_sh->setUniqueListeners(
$previousCurrentSong->setUniqueListeners(
$this->listenerRepository->getUniqueListeners(
$station,
$last_sh->getTimestampStart(),
$previousCurrentSong->getTimestampStart(),
time()
)
);
$this->em->persist($last_sh);
$this->em->persist($previousCurrentSong);
}
$sh = $this->newSongHistoryEntry($station, $song);
$newCurrentSong->setTimestampStart(time());
$this->em->persist($newCurrentSong);
$currentStreamer = $station->getCurrentStreamer();
if ($currentStreamer instanceof Entity\StationStreamer) {
$sh->setStreamer($currentStreamer);
}
$station->setCurrentSong($newCurrentSong);
$this->em->persist($station);
$sh->setTimestampStart(time());
$sh->setListenersStart($listeners);
$sh->addDeltaPoint($listeners);
$this->em->persist($sh);
$this->em->flush();
return $sh;
}
protected function newSongHistoryEntry(
Entity\Station $station,
Entity\Interfaces\SongInterface $song
): Entity\SongHistory {
// Look for an already cued but unplayed song.
$upcomingTrack = $this->stationQueueRepository->findRecentlyCuedSong($station, $song);
if (null !== $upcomingTrack) {
$this->stationQueueRepository->trackPlayed($station, $upcomingTrack);
return Entity\SongHistory::fromQueue($upcomingTrack);
}
// Check Liquidsoap's feedback cache for a record.
if (BackendAdapters::Liquidsoap === $station->getBackendTypeEnum()) {
$sh = $this->liquidsoapFeedback->registerFromFeedback($station, $song);
if (null !== $sh) {
return $sh;
}
}
return new Entity\SongHistory($station, $song);
}
public function getCurrent(Entity\Station $station): ?Entity\SongHistory
{
return $this->em->createQuery(
<<<'DQL'
SELECT sh
FROM App\Entity\SongHistory sh
WHERE sh.station = :station
AND sh.timestamp_start != 0
AND (sh.timestamp_end IS NULL OR sh.timestamp_end = 0)
ORDER BY sh.timestamp_start DESC
DQL
)->setParameter('station', $station)
->setMaxResults(1)
->getOneOrNullResult();
return $newCurrentSong;
}
/**

View File

@ -259,9 +259,14 @@ class SongHistory implements
return $this->delta_points;
}
public function addDeltaPoint(mixed $delta_point): void
public function addDeltaPoint(int $delta_point): void
{
$delta_points = (array)$this->delta_points;
if (0 === count($delta_points)) {
$this->setListenersStart($delta_point);
}
$delta_points[] = $delta_point;
$this->delta_points = $delta_points;
}
@ -277,6 +282,47 @@ class SongHistory implements
return true;
}
public function playbackEnded(): void
{
$this->setTimestampEnd(time());
$deltaPoints = (array)$this->getDeltaPoints();
if (0 !== count($deltaPoints)) {
$this->setListenersEnd(end($deltaPoints));
reset($deltaPoints);
$deltaPositive = 0;
$deltaNegative = 0;
$deltaTotal = 0;
$previousDelta = null;
foreach ($deltaPoints as $currentDelta) {
if (null !== $previousDelta) {
$deltaDelta = $currentDelta - $previousDelta;
$deltaTotal += $deltaDelta;
if ($deltaDelta > 0) {
$deltaPositive += $deltaDelta;
} elseif ($deltaDelta < 0) {
$deltaNegative += (int)abs($deltaDelta);
}
}
$previousDelta = $currentDelta;
}
$this->setDeltaPositive((int)$deltaPositive);
$this->setDeltaNegative((int)$deltaNegative);
$this->setDeltaTotal((int)$deltaTotal);
} else {
$this->setListenersEnd(0);
$this->setDeltaPositive(0);
$this->setDeltaNegative(0);
$this->setDeltaTotal(0);
}
}
public function __toString(): string
{
if ($this->media instanceof StationMedia) {

View File

@ -397,6 +397,12 @@ class Station implements Stringable, IdentifiableEntityInterface
#[ORM\OneToMany(mappedBy: 'station', targetEntity: SftpUser::class)]
protected Collection $sftp_users;
#[
ORM\ManyToOne,
ORM\JoinColumn(name: 'current_song_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')
]
protected ?SongHistory $current_song = null;
public function __construct()
{
$this->frontend_type = FrontendAdapters::Icecast->value;
@ -1114,10 +1120,22 @@ class Station implements Stringable, IdentifiableEntityInterface
return $this->sftp_users;
}
public function getCurrentSong(): ?SongHistory
{
return $this->current_song;
}
public function setCurrentSong(?SongHistory $current_song): void
{
$this->current_song = $current_song;
}
public function clearCache(): void
{
$this->nowplaying = null;
$this->nowplaying_timestamp = 0;
$this->current_song = null;
}
public function __toString(): string

View File

@ -43,7 +43,7 @@ class Queue
$expectedCueTime = CarbonImmutable::now($tzObject);
// Get expected play time of each item.
$currentSong = $this->historyRepo->getCurrent($station);
$currentSong = $station->getCurrentSong();
if (null !== $currentSong) {
$expectedPlayTime = $this->addDurationToTime(
$station,

View File

@ -7,7 +7,6 @@ namespace App\Radio\Backend\Liquidsoap\Command;
use App\Entity;
use Doctrine\ORM\EntityManagerInterface;
use Monolog\Logger;
use Psr\SimpleCache\CacheInterface;
use RuntimeException;
class FeedbackCommand extends AbstractCommand
@ -16,14 +15,40 @@ class FeedbackCommand extends AbstractCommand
Logger $logger,
protected EntityManagerInterface $em,
protected Entity\Repository\StationQueueRepository $queueRepo,
protected CacheInterface $cache
protected Entity\Repository\SongHistoryRepository $historyRepo
) {
parent::__construct($logger);
}
protected function doRun(Entity\Station $station, bool $asAutoDj = false, array $payload = []): bool
{
protected function doRun(
Entity\Station $station,
bool $asAutoDj = false,
array $payload = []
): bool {
// Process extra metadata sent by Liquidsoap (if it exists).
try {
$historyRow = $this->getSongHistory($station, $payload);
$this->historyRepo->changeCurrentSong($station, $historyRow);
$this->em->flush();
return true;
} catch (\Exception $e) {
$this->logger->error(
sprintf('Liquidsoap feedback error: %s', $e->getMessage()),
[
'exception' => $e,
]
);
return false;
}
}
private function getSongHistory(
Entity\Station $station,
array $payload
): Entity\SongHistory {
if (empty($payload['media_id'])) {
throw new RuntimeException('No payload provided.');
}
@ -33,6 +58,10 @@ class FeedbackCommand extends AbstractCommand
throw new RuntimeException('Media ID does not exist for station.');
}
if (!$this->historyRepo->isDifferentFromCurrentSong($station, $media)) {
throw new RuntimeException('Song is not different from current song.');
}
$sq = $this->queueRepo->findRecentlyCuedSong($station, $media);
if (null !== $sq) {
@ -48,48 +77,22 @@ class FeedbackCommand extends AbstractCommand
}
}
$sq->setSentToAutodj();
$sq->setTimestampPlayed(time());
$this->em->persist($sq);
$this->em->flush();
} else {
// If not, store the feedback information in the temporary cache for later checking.
$this->cache->set(
$this->getFeedbackCacheName($station),
$payload,
3600
);
}
return true;
}
$this->queueRepo->trackPlayed($station, $sq);
public function registerFromFeedback(
Entity\Station $station,
Entity\Interfaces\SongInterface $song
): ?Entity\SongHistory {
$cacheKey = $this->getFeedbackCacheName($station);
$sh = Entity\SongHistory::fromQueue($sq);
$this->em->persist($sh);
if (!$this->cache->has($cacheKey)) {
return null;
}
$extraMetadata = (array)$this->cache->get($cacheKey);
if ($song->getSongId() !== ($extraMetadata['song_id'] ?? null)) {
return null;
}
$media = $this->em->find(Entity\StationMedia::class, $extraMetadata['media_id']);
if (!$media instanceof Entity\StationMedia) {
return null;
return $sh;
}
$history = new Entity\SongHistory($station, $media);
$history->setMedia($media);
if (!empty($extraMetadata['playlist_id'])) {
$playlist = $this->em->find(Entity\StationPlaylist::class, $extraMetadata['playlist_id']);
if (!empty($payload['playlist_id'])) {
$playlist = $this->em->find(Entity\StationPlaylist::class, $payload['playlist_id']);
if ($playlist instanceof Entity\StationPlaylist) {
$history->setPlaylist($playlist);
}
@ -97,9 +100,4 @@ class FeedbackCommand extends AbstractCommand
return $history;
}
protected function getFeedbackCacheName(Entity\Station $station): string
{
return 'liquidsoap.feedback_' . $station->getIdRequired();
}
}

View File

@ -1009,6 +1009,7 @@ class ConfigWriter implements EventSubscriberInterface
# Handle "Jingle Mode" tracks by replaying the previous metadata.
last_title = ref("")
last_artist = ref("")
last_song_id = ref("")
def handle_jingle_mode(m) =
if (m["jingle_mode"] == "true") then
@ -1024,7 +1025,9 @@ class ConfigWriter implements EventSubscriberInterface
# Send metadata changes back to AzuraCast
def metadata_updated(m) =
def f() =
if (m["song_id"] != "") then
if (m["song_id"] != "" and m["song_id"] != !last_song_id) then
last_song_id := m["song_id"]
j = json()
j.add("song_id", m["song_id"])
j.add("media_id", m["media_id"])
@ -1121,12 +1124,12 @@ class ConfigWriter implements EventSubscriberInterface
}
$lsConfig[] = 'hls_streams = [' . implode(
', ',
array_map(
static fn($row) => '("' . $row . '", ' . $row . ')',
$hlsStreams
)
) . ']';
', ',
array_map(
static fn($row) => '("' . $row . '", ' . $row . ')',
$hlsStreams
)
) . ']';
$event->appendLines($lsConfig);