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:
parent
a3b0ee5ce6
commit
c90d5bdf50
|
@ -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;
|
||||
|
|
32
src/Entity/Migration/Version20220611123923.php
Normal file
32
src/Entity/Migration/Version20220611123923.php
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user