Allow switching back to "Manual AutoDJ" mode that is entirely Liquidsoap driven.

This commit is contained in:
Buster "Silver Eagle" Neece 2018-05-01 02:32:31 -05:00
parent 1f08c307e0
commit 26f1c18df4
8 changed files with 486 additions and 358 deletions

View File

@ -309,6 +309,17 @@ return [
]
],
'use_manual_autodj' => [
'radio',
[
'label' => __('Advanced: Manual AutoDJ Mode'),
'description' => __('This mode disables AzuraCast\'s AutoDJ management, using Liquidsoap itself to manage song playback. "Next Song" and some other features will not be available.'),
'default' => '0',
'options' => [0 => __('No'), 1 => __('Yes')],
'belongsTo' => 'backend_config',
]
],
'dj_port' => [
'text',
[

View File

@ -100,7 +100,7 @@ class Liquidsoap extends BackendAbstract
if (!$playlist_raw->getIsEnabled()) {
continue;
}
if ($playlist_raw->getType() === 'default') {
if ($playlist_raw->getType() === Entity\StationPlaylist::TYPE_DEFAULT) {
$has_default_playlist = true;
}
@ -126,37 +126,109 @@ class Liquidsoap extends BackendAbstract
$playlist_weights = [];
$playlist_vars = [];
$special_playlists = [
'once_per_x_songs' => [
'# Once per x Songs Playlists',
],
'once_per_x_minutes' => [
'# Once per x Minutes Playlists',
],
];
$schedule_switches = [];
foreach ($playlist_objects as $playlist) {
/** @var Entity\StationPlaylist $playlist */
$playlist_file_contents = $playlist->export('m3u', true);
$playlist_var_name = 'playlist_' . $playlist->getShortName();
$playlist_file_path = $playlist_path . '/' . $playlist_var_name . '.m3u';
file_put_contents($playlist_file_path, $playlist_file_contents);
if ($playlist->getSource() === Entity\StationPlaylist::SOURCE_SONGS) {
$playlist_file_contents = $playlist->export('m3u', true);
$playlist_file_path = $playlist_path . '/' . $playlist_var_name . '.m3u';
$ls_config[] = $playlist_var_name . ' = playlist(reload_mode="watch","' . $playlist_file_path . '")';
file_put_contents($playlist_file_path, $playlist_file_contents);
if ($playlist->getType() === 'default') {
$playlist_weights[] = $playlist->getWeight();
$playlist_vars[] = $playlist_var_name;
$playlist_mode = $playlist->getOrder() === Entity\StationPlaylist::ORDER_SEQUENTIAL
? 'normal'
: 'randomize';
$playlist_params = [
'reload_mode="watch"',
'mode="'.$playlist_mode.'"',
'"'.$playlist_file_path.'"',
];
$ls_config[] = $playlist_var_name . ' = playlist('.implode(',', $playlist_params).')';
} else {
$ls_config[] = $playlist_var_name . ' = mksafe(input.http("'.$playlist->getRemoteUrl().'"))';
}
if ($playlist->getType() === 'custom') {
if ($playlist->getType() === Entity\StationPlaylist::TYPE_ADVANCED) {
$ls_config[] = 'ignore('.$playlist_var_name.')';
}
switch($playlist->getType())
{
case Entity\StationPlaylist::TYPE_DEFAULT:
$playlist_weights[] = $playlist->getWeight();
$playlist_vars[] = $playlist_var_name;
break;
case Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS:
$special_playlists['once_per_x_songs'][] = 'radio = rotate(weights=[1,' . $playlist->getPlayPerSongs() . '], [' . $playlist_var_name . ', radio])';
break;
case Entity\StationPlaylist::TYPE_ONCE_PER_X_MINUTES:
$delay_seconds = $playlist->getPlayPerMinutes() * 60;
$special_playlists['once_per_x_minutes'][] = 'delay_' . $playlist_var_name . ' = delay(' . $delay_seconds . '., ' . $playlist['var_name'] . ')';
$special_playlists['once_per_x_minutes'][] = 'radio = fallback([delay_' . $playlist_var_name . ', radio])';
break;
case Entity\StationPlaylist::TYPE_SCHEDULED:
$play_time = $this->_getTime($playlist->getScheduleStartTime()) . '-' . $this->_getTime($playlist->getScheduleEndTime());
$schedule_switches[] = '({ ' . $play_time . ' }, ' . $playlist_var_name . ')';
break;
case Entity\StationPlaylist::TYPE_ONCE_PER_DAY:
$play_time = $this->_getTime($playlist->getPlayOnceTime());
$schedule_switches[] = '({ ' . $play_time . ' }, ' . $playlist_var_name . ')';
break;
}
}
$ls_config[] = '';
// Create fallback playlist based on all default playlists.
$ls_config[] = 'playlists = random(weights=[' . implode(', ', $playlist_weights) . '], [' . implode(', ',
// Build "default" type playlists.
$ls_config[] = '# Standard Playlists';
$ls_config[] = 'radio = random(weights=[' . implode(', ', $playlist_weights) . '], [' . implode(', ',
$playlist_vars) . ']);';
$ls_config[] = '';
$ls_config[] = 'dynamic = request.dynamic(id="azuracast_next_song", azuracast_next_song)';
$ls_config[] = 'dynamic = cue_cut(id="azuracast_next_song_cued", dynamic)';
$ls_config[] = 'radio = fallback(track_sensitive = false, [dynamic, playlists, blank(duration=2.)])';
// Add in special playlists if necessary.
foreach($special_playlists as $playlist_type => $playlist_config_lines) {
if (count($playlist_config_lines) > 1) {
$ls_config = array_merge($ls_config, $playlist_config_lines);
$ls_config[] = '';
}
}
$schedule_switches[] = '({ true }, radio)';
$ls_config[] = '# Assemble final playback order';
$fallbacks = [];
if ($this->station->useManualAutoDJ()) {
$ls_config[] = 'requests = request.queue(id="requests")';
$fallbacks[] = 'requests';
} else {
$ls_config[] = 'dynamic = request.dynamic(id="azuracast_next_song", azuracast_next_song)';
$ls_config[] = 'dynamic = cue_cut(id="azuracast_next_song_cued", dynamic)';
$fallbacks[] = 'dynamic';
}
$fallbacks[] = 'switch([ ' . implode(', ', $schedule_switches) . ' ])';
$fallbacks[] = 'blank(duration=2.)';
$ls_config[] = 'radio = fallback(track_sensitive = false, ['.implode(', ', $fallbacks).'])';
$ls_config[] = '';
// Add harbor (live DJ input) source.
@ -192,7 +264,6 @@ class Liquidsoap extends BackendAbstract
}
// Custom configuration
if (!empty($settings['custom_config'])) {
$ls_config[] = '# Custom Configuration (Specified in Station Profile)';
$ls_config[] = $settings['custom_config'];
@ -350,6 +421,39 @@ class Liquidsoap extends BackendAbstract
return $shell_path.' '.implode(' ', $shell_args);
}
/**
* Configure the time offset
*
* @param $time_code
* @return string
*/
protected function _getTime($time_code)
{
$hours = floor($time_code / 100);
$mins = $time_code % 100;
$system_time_zone = \App\Utilities::get_system_time_zone();
$system_tz = new \DateTimeZone($system_time_zone);
$system_dt = new \DateTime('now', $system_tz);
$system_offset = $system_tz->getOffset($system_dt);
$app_tz = new \DateTimeZone(date_default_timezone_get());
$app_dt = new \DateTime('now', $app_tz);
$app_offset = $app_tz->getOffset($app_dt);
$offset = $system_offset - $app_offset;
$offset_hours = floor($offset / 3600);
$hours += $offset_hours;
$hours = $hours % 24;
if ($hours < 0) {
$hours += 24;
}
return $hours . 'h' . $mins . 'm';
}
/**
* Filter a user-supplied string to be a valid LiquidSoap config entry.
*
@ -450,6 +554,24 @@ class Liquidsoap extends BackendAbstract
return '/bin/false';
}
/**
* If a station uses Manual AutoDJ mode, enqueue a request directly with Liquidsoap.
*
* @param $music_file
* @return array
* @throws \App\Exception
*/
public function request($music_file)
{
$queue = $this->command('requests.queue');
if (!empty($queue[0])) {
throw new \Exception('Song(s) still pending in request queue.');
}
return $this->command('requests.push ' . $music_file);
}
/**
* Tell LiquidSoap to skip the currently playing song.
*
@ -584,9 +706,7 @@ class Liquidsoap extends BackendAbstract
$sh = $history_repo->getNextSongForStation($this->station, $as_autodj);
if ($sh instanceof Entity\SongHistory) {
// 'annotate:type=\"song\",album=\"$ALBUM\",display_desc=\"$FULLSHOWNAME\",liq_start_next=\"2.5\",liq_fade_in=\"3.5\",liq_fade_out=\"3.5\":$SONGPATH'
$media = $sh->getMedia();
if ($media instanceof Entity\StationMedia) {
$song_path = $media->getFullPath();
return 'annotate:' . implode(',', $media->getAnnotations()) . ':' . $song_path;

View File

@ -2,7 +2,6 @@
namespace AzuraCast\Sync;
use App\Cache;
use App\Debug;
use App\Url;
use AzuraCast\ApiUtilities;
use AzuraCast\Radio\Adapters;
@ -168,7 +167,7 @@ class NowPlaying extends SyncAbstract
$offline_sh->song = $song_obj->api($this->api_utils);
$np->now_playing = $offline_sh;
$np->song_history = $this->history_repo->getHistoryForStation($station, $this->url);
$np->song_history = $this->history_repo->getHistoryForStation($station, $this->api_utils);
$next_song = $this->history_repo->getNextSongForStation($station);
if ($next_song instanceof Entity\SongHistory) {

View File

@ -1,12 +1,17 @@
<?php
namespace Entity\Repository;
use Doctrine\ORM\NoResultException;
use Entity;
class SongHistoryRepository extends BaseRepository
{
public function getNextSongForStation(Entity\Station $station, $is_autodj = false)
{
if ($station->useManualAutoDJ()) {
return null;
}
$next_song = $this->_em->createQuery('SELECT sh, s, sm
FROM ' . $this->_entityName . ' sh JOIN sh.song s JOIN sh.media sm
WHERE sh.station_id = :station_id
@ -21,10 +26,7 @@ class SongHistoryRepository extends BaseRepository
->getOneOrNullResult();
if (!($next_song instanceof Entity\SongHistory)) {
/** @var Entity\Repository\StationMediaRepository $media_repo */
$media_repo = $this->_em->getRepository(Entity\StationMedia::class);
$next_song = $media_repo->getNextSong($station);
$next_song = $this->getNextSong($station);
}
if ($next_song instanceof Entity\SongHistory && $is_autodj) {
@ -38,6 +40,8 @@ class SongHistoryRepository extends BaseRepository
}
/**
* @param Entity\Station $station
* @param \AzuraCast\ApiUtilities $api_utils
* @param int $num_entries
* @return array
*/
@ -155,4 +159,316 @@ class SongHistoryRepository extends BaseRepository
return $sh;
}
}
/**
* Determine the next-playing song for this station based on its playlist rotation rules.
*
* @param Entity\Station $station
* @return Entity\SongHistory|null
*/
public function getNextSong(Entity\Station $station)
{
// Process requests first (if applicable)
if ($station->getEnableRequests()) {
$min_minutes = (int)$station->getRequestDelay();
$threshold_minutes = $min_minutes + mt_rand(0, $min_minutes);
$threshold = time() - ($threshold_minutes * 60);
// Look up all requests that have at least waited as long as the threshold.
$request = $this->_em->createQuery('SELECT sr, sm
FROM Entity\StationRequest sr JOIN sr.track sm
WHERE sr.played_at = 0 AND sr.station_id = :station_id AND sr.timestamp <= :threshold
ORDER BY sr.id ASC')
->setParameter('station_id', $station->getId())
->setParameter('threshold', $threshold)
->setMaxResults(1)
->getOneOrNullResult();
if ($request instanceof Entity\StationRequest) {
return $this->_playSongFromRequest($request);
}
}
// Pull all active, non-empty playlists and sort by type.
$playlists_by_type = [];
foreach($station->getPlaylists() as $playlist) {
/** @var Entity\StationPlaylist $playlist */
// Don't include empty playlists or nonstandard ones
if ($playlist->getIsEnabled()
&& $playlist->getSource() === Entity\StationPlaylist::SOURCE_SONGS
&& $playlist->getMediaItems()->count() > 0) {
$playlists_by_type[$playlist->getType()][$playlist->getId()] = $playlist;
}
}
// Pull all recent cued songs for easy referencing below.
$cued_song_history = $this->_em->createQuery('SELECT sh FROM '.$this->_entityName.' sh
WHERE sh.station_id = :station_id
AND (sh.timestamp_cued != 0 AND sh.timestamp_cued IS NOT NULL)
AND sh.timestamp_cued >= :threshold
ORDER BY sh.timestamp_cued DESC')
->setParameter('station_id', $station->getId())
->setParameter('threshold', time()-86399)
->getArrayResult();
// Once per day playlists
if (!empty($playlists_by_type['once_per_day'])) {
foreach ($playlists_by_type['once_per_day'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
if ($playlist->canPlayOnce()) {
// Check if already played
$relevant_song_history = array_slice($cued_song_history, 0, 15);
$was_played = false;
foreach($relevant_song_history as $sh_row) {
if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
}
// Once per X songs playlists
if (!empty($playlists_by_type['once_per_x_songs'])) {
foreach($playlists_by_type['once_per_x_songs'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
$relevant_song_history = array_slice($cued_song_history, 0, $playlist->getPlayPerSongs());
$was_played = false;
foreach($relevant_song_history as $sh_row) {
if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
// Once per X minutes playlists
if (!empty($playlists_by_type['once_per_x_minutes'])) {
foreach($playlists_by_type['once_per_x_minutes'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
$threshold = time() - ($playlist->getPlayPerMinutes() * 60);
$was_played = false;
foreach($cued_song_history as $sh_row) {
if ($sh_row['timestamp_cued'] < $threshold) {
break;
} else if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
// Time-block scheduled playlists
if (!empty($playlists_by_type['scheduled'])) {
foreach ($playlists_by_type['scheduled'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
if ($playlist->canPlayScheduled()) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
}
}
// Default rotation playlists
if (!empty($playlists_by_type['default'])) {
$playlist_weights = [];
foreach($playlists_by_type['default'] as $playlist_id => $playlist) {
/** @var Entity\StationPlaylist $playlist */
$playlist_weights[$playlist_id] = $playlist->getWeight();
}
$rand = random_int(1, (int)array_sum($playlist_weights));
foreach ($playlist_weights as $playlist_id => $weight) {
$rand -= $weight;
if ($rand <= 0) {
$playlist = $playlists_by_type['default'][$playlist_id];
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
}
}
return null;
}
protected function _playSongFromRequest(Entity\StationRequest $request)
{
// Log in history
$sh = new Entity\SongHistory($request->getTrack()->getSong(), $request->getStation());
$sh->setRequest($request);
$sh->setMedia($request->getTrack());
$sh->setDuration($request->getTrack()->getCalculatedLength());
$sh->setTimestampCued(time());
$this->_em->persist($sh);
$request->setPlayedAt(time());
$this->_em->persist($request);
$this->_em->flush();
return $sh;
}
protected function _playSongFromPlaylist(Entity\StationPlaylist $playlist)
{
if ($playlist->getOrder() === Entity\StationPlaylist::ORDER_SEQUENTIAL) {
$media_to_play = $this->_playSequentialSongFromPlaylist($playlist);
} else {
$media_to_play = $this->_playRandomSongFromPlaylist($playlist);
}
if ($media_to_play instanceof Entity\StationMedia) {
$spm = $media_to_play->getItemForPlaylist($playlist);
$spm->played();
$this->_em->persist($spm);
// Log in history
$sh = new Entity\SongHistory($media_to_play->getSong(), $playlist->getStation());
$sh->setPlaylist($playlist);
$sh->setMedia($media_to_play);
$sh->setDuration($media_to_play->getCalculatedLength());
$sh->setTimestampCued(time());
$this->_em->persist($sh);
$this->_em->flush();
return $sh;
}
return null;
}
protected function _playRandomSongFromPlaylist(Entity\StationPlaylist $playlist)
{
// Get some random songs from playlist.
$random_songs = $this->_em->createQuery('SELECT sm, spm, s, st FROM Entity\StationMedia sm
JOIN sm.song s
JOIN sm.station st
JOIN sm.playlist_items spm
JOIN spm.playlist sp
WHERE spm.playlist_id = :playlist_id
GROUP BY sm.id ORDER BY RAND()')
->setParameter('playlist_id', $playlist->getId())
->setMaxResults(15)
->execute();
/** @var bool Whether to use "last song ID played" or "genuine random shuffle" mode. */
$use_song_ids = true;
// Get all song IDs from the random songs.
$song_timestamps = [];
$songs_by_id = [];
foreach($random_songs as $media_row) {
/** @var Entity\StationMedia $media_row */
if ($media_row->getLength() == 0) {
$use_song_ids = false;
break;
} else {
$playlist_item = $media_row->getItemForPlaylist($playlist);
$song_timestamps[$media_row->getSong()->getId()] = $playlist_item->getLastPlayed();
$songs_by_id[$media_row->getSong()->getId()] = $media_row;
}
}
if ($use_song_ids) {
asort($song_timestamps);
reset($song_timestamps);
$id_to_play = key($song_timestamps);
$random_song = $songs_by_id[$id_to_play];
} else {
shuffle($random_songs);
$random_song = array_pop($random_songs);
}
return $random_song;
}
protected function _playSequentialSongFromPlaylist(Entity\StationPlaylist $playlist)
{
// Fetch the most recently played song
try {
/** @var Entity\StationPlaylistMedia $last_played_media */
$last_played_media = $this->_em->createQuery('SELECT spm FROM Entity\StationPlaylistMedia spm
WHERE spm.playlist_id = :playlist_id
ORDER BY spm.last_played DESC')
->setParameter('playlist_id', $playlist->getId())
->setMaxResults(1)
->getSingleResult();
} catch(NoResultException $e) {
return null;
}
$last_weight = (int)$last_played_media->getWeight();
// Try to find a song of greater weight. If none exists, start back with zero.
$next_song_query = $this->_em->createQuery('SELECT spm, sm, s, st FROM Entity\StationPlaylistMedia spm
JOIN spm.media sm
JOIN sm.song s
JOIN sm.station st
WHERE spm.weight >= :weight
ORDER BY spm.weight ASC')
->setMaxResults(1);
try {
$next_song = $next_song_query
->setParameter('weight', $last_weight+1)
->getSingleResult();
} catch(NoResultException $e) {
$next_song = $next_song_query
->setParameter('weight', 0)
->getSingleResult();
}
/** @var Entity\StationPlaylistMedia $next_song */
return $next_song->getMedia();
}
}

View File

@ -1,7 +1,6 @@
<?php
namespace Entity\Repository;
use Doctrine\ORM\NoResultException;
use Entity;
class StationMediaRepository extends BaseRepository
@ -72,319 +71,6 @@ class StationMediaRepository extends BaseRepository
return $record;
}
/**
* Determine the next-playing song for this station based on its playlist rotation rules.
*
* @param Entity\Station $station
* @return Entity\SongHistory|null
*/
public function getNextSong(Entity\Station $station)
{
// Process requests first (if applicable)
if ($station->getEnableRequests()) {
$min_minutes = (int)$station->getRequestDelay();
$threshold_minutes = $min_minutes + mt_rand(0, $min_minutes);
$threshold = time() - ($threshold_minutes * 60);
// Look up all requests that have at least waited as long as the threshold.
$request = $this->_em->createQuery('SELECT sr, sm
FROM Entity\StationRequest sr JOIN sr.track sm
WHERE sr.played_at = 0 AND sr.station_id = :station_id AND sr.timestamp <= :threshold
ORDER BY sr.id ASC')
->setParameter('station_id', $station->getId())
->setParameter('threshold', $threshold)
->setMaxResults(1)
->getOneOrNullResult();
if ($request instanceof Entity\StationRequest) {
return $this->_playSongFromRequest($request);
}
}
// Pull all active, non-empty playlists and sort by type.
$playlists_by_type = [];
foreach($station->getPlaylists() as $playlist) {
/** @var Entity\StationPlaylist $playlist */
// Don't include empty playlists
if ($playlist->canBeCued()) {
$playlists_by_type[$playlist->getType()][$playlist->getId()] = $playlist;
}
}
// Pull all recent cued songs for easy referencing below.
$cued_song_history = $this->_em->createQuery('SELECT sh FROM Entity\SongHistory sh
WHERE sh.station_id = :station_id
AND (sh.timestamp_cued != 0 AND sh.timestamp_cued IS NOT NULL)
AND sh.timestamp_cued >= :threshold
ORDER BY sh.timestamp_cued DESC')
->setParameter('station_id', $station->getId())
->setParameter('threshold', time()-86399)
->getArrayResult();
// Once per day playlists
if (!empty($playlists_by_type['once_per_day'])) {
foreach ($playlists_by_type['once_per_day'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
if ($playlist->canPlayOnce()) {
// Check if already played
$relevant_song_history = array_slice($cued_song_history, 0, 15);
$was_played = false;
foreach($relevant_song_history as $sh_row) {
if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
}
// Once per X songs playlists
if (!empty($playlists_by_type['once_per_x_songs'])) {
foreach($playlists_by_type['once_per_x_songs'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
$relevant_song_history = array_slice($cued_song_history, 0, $playlist->getPlayPerSongs());
$was_played = false;
foreach($relevant_song_history as $sh_row) {
if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
// Once per X minutes playlists
if (!empty($playlists_by_type['once_per_x_minutes'])) {
foreach($playlists_by_type['once_per_x_minutes'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
$threshold = time() - ($playlist->getPlayPerMinutes() * 60);
$was_played = false;
foreach($cued_song_history as $sh_row) {
if ($sh_row['timestamp_cued'] < $threshold) {
break;
} else if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
// Time-block scheduled playlists
if (!empty($playlists_by_type['scheduled'])) {
foreach ($playlists_by_type['scheduled'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
if ($playlist->canPlayScheduled()) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
}
}
// Default rotation playlists
if (!empty($playlists_by_type['default'])) {
$playlist_weights = [];
foreach($playlists_by_type['default'] as $playlist_id => $playlist) {
/** @var Entity\StationPlaylist $playlist */
$playlist_weights[$playlist_id] = $playlist->getWeight();
}
$rand = random_int(1, (int)array_sum($playlist_weights));
foreach ($playlist_weights as $playlist_id => $weight) {
$rand -= $weight;
if ($rand <= 0) {
$playlist = $playlists_by_type['default'][$playlist_id];
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
}
}
return null;
}
protected function _playSongFromRequest(Entity\StationRequest $request)
{
// Log in history
$sh = new Entity\SongHistory($request->getTrack()->getSong(), $request->getStation());
$sh->setRequest($request);
$sh->setMedia($request->getTrack());
$sh->setDuration($request->getTrack()->getCalculatedLength());
$sh->setTimestampCued(time());
$this->_em->persist($sh);
$request->setPlayedAt(time());
$this->_em->persist($request);
$this->_em->flush();
return $sh;
}
protected function _playSongFromPlaylist(Entity\StationPlaylist $playlist)
{
if ($playlist->getSource() === Entity\StationPlaylist::SOURCE_SONGS) {
if ($playlist->getOrder() === Entity\StationPlaylist::ORDER_SEQUENTIAL) {
$media_to_play = $this->_playSequentialSongFromPlaylist($playlist);
} else {
$media_to_play = $this->_playRandomSongFromPlaylist($playlist);
}
if ($media_to_play instanceof Entity\StationMedia) {
$spm = $media_to_play->getItemForPlaylist($playlist);
$spm->played();
$this->_em->persist($spm);
// Log in history
$sh = new Entity\SongHistory($media_to_play->getSong(), $playlist->getStation());
$sh->setPlaylist($playlist);
$sh->setMedia($media_to_play);
$sh->setDuration($media_to_play->getCalculatedLength());
$sh->setTimestampCued(time());
$this->_em->persist($sh);
$this->_em->flush();
return $sh;
}
} else {
return $playlist->getRemoteUrl();
}
return null;
}
protected function _playRandomSongFromPlaylist(Entity\StationPlaylist $playlist)
{
// Get some random songs from playlist.
$random_songs = $this->_em->createQuery('SELECT sm, spm, s, st FROM Entity\StationMedia sm
JOIN sm.song s
JOIN sm.station st
JOIN sm.playlist_items spm
JOIN spm.playlist sp
WHERE spm.playlist_id = :playlist_id
GROUP BY sm.id ORDER BY RAND()')
->setParameter('playlist_id', $playlist->getId())
->setMaxResults(15)
->execute();
/** @var bool Whether to use "last song ID played" or "genuine random shuffle" mode. */
$use_song_ids = true;
// Get all song IDs from the random songs.
$song_timestamps = [];
$songs_by_id = [];
foreach($random_songs as $media_row) {
/** @var Entity\StationMedia $media_row */
if ($media_row->getLength() == 0) {
$use_song_ids = false;
break;
} else {
$playlist_item = $media_row->getItemForPlaylist($playlist);
$song_timestamps[$media_row->getSong()->getId()] = $playlist_item->getLastPlayed();
$songs_by_id[$media_row->getSong()->getId()] = $media_row;
}
}
if ($use_song_ids) {
asort($song_timestamps);
reset($song_timestamps);
$id_to_play = key($song_timestamps);
$random_song = $songs_by_id[$id_to_play];
} else {
shuffle($random_songs);
$random_song = array_pop($random_songs);
}
return $random_song;
}
protected function _playSequentialSongFromPlaylist(Entity\StationPlaylist $playlist)
{
// Fetch the most recently played song
try {
/** @var Entity\StationPlaylistMedia $last_played_media */
$last_played_media = $this->_em->createQuery('SELECT spm FROM Entity\StationPlaylistMedia spm
WHERE spm.playlist_id = :playlist_id
ORDER BY spm.last_played DESC')
->setParameter('playlist_id', $playlist->getId())
->setMaxResults(1)
->getSingleResult();
} catch(NoResultException $e) {
return null;
}
$last_weight = (int)$last_played_media->getWeight();
// Try to find a song of greater weight. If none exists, start back with zero.
$next_song_query = $this->_em->createQuery('SELECT spm, sm, s, st FROM Entity\StationPlaylistMedia spm
JOIN spm.media sm
JOIN sm.song s
JOIN sm.station st
WHERE spm.weight >= :weight
ORDER BY spm.weight ASC')
->setMaxResults(1);
try {
$next_song = $next_song_query
->setParameter('weight', $last_weight+1)
->getSingleResult();
} catch(NoResultException $e) {
$next_song = $next_song_query
->setParameter('weight', 0)
->getSingleResult();
}
/** @var Entity\StationPlaylistMedia $next_song */
return $next_song->getMedia();
}
/**
* Retrieve a key-value representation of all custom metadata for the specified media.
*

View File

@ -408,6 +408,17 @@ class Station
$this->backend_config = $config;
}
/**
* Whether the station uses AzuraCast to directly manage the AutoDJ or lets the backend handle it.
*
* @return bool
*/
public function useManualAutoDJ(): bool
{
$settings = (array)$this->getBackendConfig();
return (bool)$settings['use_manual_autodj'] ?? false;
}
/**
* @return null|string
*/

View File

@ -513,6 +513,10 @@ class StationMedia
/**
* Assemble a list of annotations for LiquidSoap.
*
* Liquidsoap expects a string similar to:
* annotate:type="song",album="$ALBUM",display_desc="$FULLSHOWNAME",
* liq_start_next="2.5",liq_fade_in="3.5",liq_fade_out="3.5":$SONGPATH
*
* @return array
*/
public function getAnnotations(): array

View File

@ -152,6 +152,7 @@ class StationPlaylist
/**
* @OneToMany(targetEntity="StationPlaylistMedia", mappedBy="playlist", fetch="EXTRA_LAZY")
* @OrderBy({"weight" = "ASC"})
* @var Collection
*/
protected $media_items;
@ -558,24 +559,6 @@ class StationPlaylist
return $this->media_items;
}
/**
* Returns whether a playlist is sufficiently populated to be "cued" as a next song for a station.
*
* @return bool
*/
public function canBeCued(): bool
{
if (!$this->is_enabled) {
return false;
}
if ($this->source === self::SOURCE_REMOTE_URL) {
return !empty($this->remote_url);
}
return $this->media_items->count() > 0;
}
/**
* Export the playlist into a reusable format.
*
@ -597,8 +580,6 @@ class StationPlaylist
$playlist_file[] = $media_file_path;
}
shuffle($playlist_file);
return implode("\n", $playlist_file);
break;