Allow switching back to "Manual AutoDJ" mode that is entirely Liquidsoap driven.
This commit is contained in:
parent
1f08c307e0
commit
26f1c18df4
|
@ -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',
|
||||
[
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue