4
0
mirror of https://github.com/AzuraCast/AzuraCast.git synced 2024-06-18 15:07:05 +00:00

Closes #38 - Implement support for several new playlist types: scheduled, once per x songs, once per x minutes, once per day at time x.

This commit is contained in:
Buster Silver 2016-09-21 02:53:48 -05:00
parent d8b7d0c2e5
commit 5928005176
10 changed files with 453 additions and 67 deletions

View File

@ -72,7 +72,9 @@ class Automation
foreach($station->playlists as $playlist)
{
if ($playlist->include_in_automation)
if ($playlist->is_enabled &&
$playlist->type == 'default' &&
$playlist->include_in_automation == true)
{
// Clear all related media.
foreach($playlist->media as $media)

View File

@ -44,16 +44,17 @@ class LiquidSoap extends AdapterAbstract
@unlink($playlist_path.'/'.$list);
// Write new playlists.
$playlist_weights = array();
$playlist_vars = array();
$playlists_by_type = array();
$ls_config[] = '# Playlists';
foreach($this->station->playlists as $playlist)
foreach($this->station->playlists as $playlist_raw)
{
$playlist_file = array();
if (!$playlist_raw->is_enabled)
continue;
foreach($playlist->media as $media_file)
$playlist_file = array();
foreach($playlist_raw->media as $media_file)
{
$media_file_path = $media_path.'/'.$media_file->path;
$playlist_file[] = $media_file_path;
@ -61,31 +62,108 @@ class LiquidSoap extends AdapterAbstract
$playlist_file_contents = implode("\n", $playlist_file);
$playlist_var_name = 'playlist_'.$playlist->getShortName();
$playlist_file_path = $playlist_path.'/'.$playlist_var_name.'.pls';
$playlist = $playlist_raw->toArray();
file_put_contents($playlist_file_path, $playlist_file_contents);
$playlist['var_name'] = 'playlist_'.$playlist_raw->getShortName();
$playlist['file_path'] = $playlist_path.'/'.$playlist['var_name'].'.pls';
$ls_config[] = $playlist_var_name.' = playlist(reload=1800,"'.$playlist_file_path.'")';
file_put_contents($playlist['file_path'], $playlist_file_contents);
$playlist_weights[] = $playlist->weight;
$playlist_vars[] = $playlist_var_name;
$ls_config[] = $playlist['var_name'].' = playlist(reload=1800,"'.$playlist['file_path'].'")';
$playlist_type = $playlist['type'] ?: 'default';
$playlists_by_type[$playlist_type][] = $playlist;
}
$ls_config[] = '';
$ls_config[] = '# Build Radio Station';
$ls_config[] = 'radio = random(weights = ['.implode(', ', $playlist_weights).'],['.implode(', ', $playlist_vars).']);';
$ls_config[] = '';
// Cannot build a LiquidSoap playlist with
if (count($playlists_by_type['default']) == 0)
return false;
// Build "default" type playlists.
$playlist_weights = array();
$playlist_vars = array();
foreach($playlists_by_type['default'] as $playlist)
{
$playlist_weights[] = $playlist['weight'];
$playlist_vars[] = $playlist['var_name'];
}
$ls_config[] = '# Standard Playlists';
$ls_config[] = 'radio = random(weights=['.implode(', ', $playlist_weights).'], ['.implode(', ', $playlist_vars).']);';
$ls_config[] = '';
// Once per X songs playlists
if (count($playlists_by_type['once_per_x_songs']) > 0)
{
$ls_config[] = '# Once per x Songs Playlists';
foreach($playlists_by_type['once_per_x_songs'] as $playlist)
{
$ls_config[] = 'radio = rotate(weights=[1,' . $playlist['play_per_songs'] . '], [' . $playlist['var_name'] . ', radio])';
}
$ls_config[] = '';
}
// Once per X minutes playlists
if (count($playlists_by_type['once_per_x_minutes']) > 0)
{
$ls_config[] = '# Once per x Minutes Playlists';
foreach($playlists_by_type['once_per_x_minutes'] as $playlist)
{
$delay_seconds = $playlist['play_per_minutes']*60;
$ls_config[] = 'delay_'.$playlist['var_name'].' = delay('.$delay_seconds.'., '.$playlist['var_name'].')';
$ls_config[] = 'radio = fallback([delay_'.$playlist['var_name'].', radio])';
}
$ls_config[] = '';
}
// Set up "switch" conditionals
$switches = [];
// Scheduled playlists
if (count($playlists_by_type['scheduled']) > 0)
{
foreach($playlists_by_type['scheduled'] as $playlist)
{
$play_time = $this->_getTime($playlist['schedule_start_time']).'-'.$this->_getTime($playlist['schedule_end_time']);
$switches[] = '({ ' . $play_time . ' }, ' . $playlist['var_name'] . ')';
}
}
// Once per day playlists
if (count($playlists_by_type['once_per_day']) > 0)
{
foreach($playlists_by_type['once_per_day'] as $playlist)
{
$play_time = $this->_getTime($playlist['play_once_time']);
$switches[] = '({ ' . $play_time . ' }, ' . $playlist['var_name'] . ')';
}
}
// Add fallback error file.
$error_song_path = APP_INCLUDE_ROOT.'/resources/error.mp3';
$ls_config[] = '';
$ls_config[] = '# Fallback Media File';
$ls_config[] = '# Assemble Fallback';
// $ls_config[] = 'security = single("'.$error_song_path.'")';
$ls_config[] = 'requests = request.queue(id="requests")';
$fallbacks = [];
$fallbacks[] = 'requests';
$switches[] = '({ true }, radio)';
$fallbacks[] = 'switch([ '.implode(', ', $switches).' ])';
$fallbacks[] = 'blank(duration=2.)';
// $ls_config[] = 'radio = fallback(track_sensitive = true, [playlists, security])';
$ls_config[] = 'radio = fallback(track_sensitive = true, [requests, radio, blank(duration=2.)])';
$ls_config[] = 'radio = fallback(track_sensitive = true, ['.implode(', ', $fallbacks).'])';
$ls_config[] = '';
$ls_config[] = '# Crossfading';
@ -125,6 +203,32 @@ class LiquidSoap extends AdapterAbstract
return true;
}
protected function _getTime($time_code)
{
$hours = floor($time_code / 100);
$mins = $time_code % 100;
$system_time_zone = \App\Utilities::getSystemTimeZone();
$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';
}
public function isRunning()
{
$config_path = $this->station->getRadioConfigDir();

View File

@ -1042,4 +1042,37 @@ class Utilities
return false;
}
/**
* Get the system time zone.
* @return string
*/
public static function getSystemTimeZone()
{
if (file_exists('/etc/timezone'))
{
// Ubuntu / Debian.
$data = file_get_contents('/etc/timezone');
if ($data)
return trim($data);
}
elseif (is_link('/etc/localtime'))
{
// Mac OS X (and older Linuxes)
// /etc/localtime is a symlink to the
// timezone in /usr/share/zoneinfo.
$filename = readlink('/etc/localtime');
if (strpos($filename, '/usr/share/zoneinfo/') === 0)
return substr($filename, 20);
}
elseif (file_exists('/etc/sysconfig/clock'))
{
// RHEL / CentOS
$data = parse_ini_file('/etc/sysconfig/clock');
if (!empty($data['ZONE']))
return trim($data['ZONE']);
}
return 'UTC';
}
}

View File

@ -176,7 +176,7 @@ class Station extends \App\Doctrine\Entity
/**
* @OneToMany(targetEntity="StationPlaylist", mappedBy="station")
* @OrderBy({"weight" = "DESC"})
* @OrderBy({"type" = "ASC","weight" = "DESC"})
*/
protected $playlists;

View File

@ -12,8 +12,11 @@ class StationPlaylist extends \App\Doctrine\Entity
{
public function __construct()
{
$this->include_in_automation = false;
$this->type = 'default';
$this->weight = 3;
$this->is_enabled = 1;
$this->include_in_automation = false;
$this->media = new ArrayCollection;
}
@ -36,6 +39,42 @@ class StationPlaylist extends \App\Doctrine\Entity
return Station::getStationShortName($this->name);
}
/** @Column(name="type", type="string", length=50) */
protected $type;
/** @Column(name="is_enabled", type="boolean", nullable=false) */
protected $is_enabled;
/** @Column(name="play_per_songs", type="smallint") */
protected $play_per_songs;
/** @Column(name="play_per_minutes", type="smallint") */
protected $play_per_minutes;
/** @Column(name="schedule_start_time", type="smallint") */
protected $schedule_start_time;
public function getScheduleStartTimeText()
{
return self::formatTimeCode($this->schedule_start_time);
}
/** @Column(name="schedule_end_time", type="smallint") */
protected $schedule_end_time;
public function getScheduleEndTimeText()
{
return self::formatTimeCode($this->schedule_end_time);
}
/** @Column(name="play_once_time", type="smallint") */
protected $play_once_time;
public function getPlayOnceTimeText()
{
return self::formatTimeCode($this->play_once_time);
}
/** @Column(name="weight", type="smallint") */
protected $weight;
@ -54,4 +93,25 @@ class StationPlaylist extends \App\Doctrine\Entity
* @ManyToMany(targetEntity="StationMedia", mappedBy="playlists", fetch="EXTRA_LAZY")
*/
protected $media;
/**
* Given a time code i.e. "2300", return a time i.e. "11:00 PM"
* @param $time_code
*/
public static function formatTimeCode($time_code)
{
$hours = floor($time_code / 100);
$mins = $time_code % 100;
$ampm = ($hours < 12) ? 'AM' : 'PM';
if ($hours == 0)
$hours_text = '12';
elseif ($hours > 12)
$hours_text = $hours-12;
else
$hours_text = $hours;
return $hours_text.':'.str_pad($mins, 2, '0', STR_PAD_LEFT).' '.$ampm;
}
}

View File

@ -1,47 +1,179 @@
<?php
$hour_select = [];
for($hr = 0; $hr <= 23; $hr++)
{
foreach([0, 15, 30, 45] as $min)
{
$time_num = $hr*100 + $min;
$hour_select[$time_num] = \Entity\StationPlaylist::formatTimeCode($time_num);
}
}
return [
'method' => 'post',
'enctype' => 'multipart/form-data',
'elements' => [
'groups' => [
'name' => ['text', [
'label' => 'Playlist Name',
'required' => true,
]],
'basic_info' => [
'elements' => [
'name' => ['text', [
'label' => 'Playlist Name',
'required' => true,
]],
'is_enabled' => ['radio', [
'label' => 'Enable Playlist',
'required' => true,
'description' => 'If set to "No", the playlist will not be included in radio playback, but can still be managed.',
'options' => [
1 => 'Yes',
0 => 'No',
],
'default' => 1,
]],
'type' => ['radio', [
'label' => 'Playlist Type',
'options' => [
'default' => '<b>Standard:</b> Plays all day, shuffles with other standard playlists based on weight.',
'scheduled' => '<b>Scheduled:</b> Play during a scheduled time range. Useful for mood-based time playlists.',
'once_per_x_songs' => '<b>Once per x Songs:</b> Play exactly once every <i>x</i> songs. Useful for station ID/jingles.',
'once_per_x_minutes' => '<b>Once Per x Minutes:</b> Play exactly once every <i>x</i> minutes. Useful for station ID/jingles.',
'once_per_day' => '<b>Daily</b>: Play once per day at the specified time. Useful for timely reminders.',
],
'default' => 'default',
'required' => true,
]],
'weight' => ['radio', [
'label' => 'Playlist Weight',
'description' => 'How often the playlist\'s songs will be played. 1 is the most infrequent, 5 is the most frequent.',
'default' => 3,
'required' => true,
'class' => 'inline',
'options' => [
1 => '1 - Lowest',
2 => '2',
3 => '3 - Default',
4 => '4',
5 => '5 - Highest',
],
]],
],
'type_default' => [
'legend' => 'Standard Playlist',
'class' => 'type_fieldset',
'elements' => [
'weight' => ['radio', [
'label' => 'Playlist Weight',
'description' => 'How often the playlist\'s songs will be played. 1 is the most infrequent, 5 is the most frequent.',
'default' => 3,
'required' => true,
'class' => 'inline',
'options' => [
1 => '1 - Lowest',
2 => '2',
3 => '3 - Default',
4 => '4',
5 => '5 - Highest',
],
]],
'include_in_automation' => ['radio', [
'label' => 'Include in Automated Assignment',
'description' => 'If auto-assignment is enabled, use this playlist as one of the targets for songs to be redistributed into. This will overwrite the existing contents of this playlist.',
'required' => true,
'default' => '0',
'options' => [
0 => 'No',
1 => 'Yes',
],
]],
'include_in_automation' => ['radio', [
'label' => 'Include in Automated Assignment',
'description' => 'If auto-assignment is enabled, use this playlist as one of the targets for songs to be redistributed into. This will overwrite the existing contents of this playlist.',
'required' => true,
'default' => '0',
'options' => [
0 => 'No',
1 => 'Yes',
],
]],
],
'submit' => ['submit', [
'type' => 'submit',
'label' => 'Save Changes',
'helper' => 'formButton',
'class' => 'ui-button btn-lg btn-primary',
]],
'type_scheduled' => [
'legend' => 'Scheduled Playlist',
'class' => 'type_fieldset',
'elements' => [
'schedule_start_time' => ['select', [
'label' => 'Start Time',
'description' => 'Current server time is <b>'.date('g:ia').'.</b>',
'options' => $hour_select,
]],
'schedule_end_time' => ['select', [
'label' => 'End Time',
'description' => 'If the end time is before the start time, the playlist will play overnight until this time on the next day.',
'options' => $hour_select,
]],
],
],
'type_once_per_x_songs' => [
'legend' => 'Once per x Songs Playlist',
'class' => 'type_fieldset',
'elements' => [
'play_per_songs' => ['radio', [
'label' => 'Number of Songs Between Plays',
'description' => 'This playlist will play every $x songs, where $x is specified below.',
'options' => \App\Utilities::pairs([
5,
10,
15,
20,
25,
50,
100
]),
]],
],
],
'type_once_per_x_minutes' => [
'legend' => 'Once per x Minutes Playlist',
'class' => 'type_fieldset',
'elements' => [
'play_per_minutes' => ['radio', [
'label' => 'Number of Minutes Between Plays',
'description' => 'This playlist will play every $x minutes, where $x is specified below.',
'options' => \App\Utilities::pairs([
5,
10,
15,
30,
45,
60,
120,
240,
]),
]],
],
],
'type_once_per_day' => [
'legend' => 'Daily Playlist',
'class' => 'type_fieldset',
'elements' => [
'play_once_time' => ['select', [
'label' => 'Scheduled Play Time',
'description' => 'Current server time is <b>'.date('g:ia').'.</b>',
'options' => $hour_select,
]],
],
],
'grp_submit' => [
'elements' => [
'submit' => ['submit', [
'type' => 'submit',
'label' => 'Save Changes',
'helper' => 'formButton',
'class' => 'ui-button btn-lg btn-primary',
]],
],
],
],
];

View File

@ -12,13 +12,19 @@ class PlaylistsController extends BaseController
$total_weights = 0;
foreach($all_playlists as $playlist)
$total_weights += $playlist->weight;
{
if ($playlist->is_enabled && $playlist->type == 'default')
$total_weights += $playlist->weight;
}
$playlists = array();
foreach($all_playlists as $playlist)
{
$playlist_row = $playlist->toArray();
$playlist_row['probability'] = round(($playlist->weight / $total_weights) * 100, 1).'%';
if ($playlist->is_enabled && $playlist->type == 'default')
$playlist_row['probability'] = round(($playlist->weight / $total_weights) * 100, 1).'%';
$playlist_row['num_songs'] = count($playlist->media);
$playlists[$playlist->id] = $playlist_row;
@ -62,16 +68,15 @@ class PlaylistsController extends BaseController
$record->fromArray($data);
$record->save();
if ($reload_station)
$this->_reloadStation();
$this->_reloadStation();
$this->alert('<b>Stream updated!</b>', 'green');
return $this->redirectFromHere(['action' => 'index', 'id' => NULL]);
}
$title = (($this->hasParam('id')) ? 'Edit' : 'Add').' Playlist';
return $this->renderForm($form, 'edit', $title);
$this->view->form = $form;
$this->view->title = (($this->hasParam('id')) ? 'Edit' : 'Add').' Playlist';
}
public function deleteAction()

View File

@ -0,0 +1,31 @@
<?php $this->layout('main', ['manual' => true]) ?>
<div class="block-header">
<h2><?=$station->name ?></h2>
</div>
<div class="card">
<div class="card-header ch-alt">
<h2><?=$title ?></h2>
<?=$this->insert('system/form') ?>
</div>
</div>
<script type="text/javascript">
$(function() {
showFieldset($('form #field_type input:checked').val());
$('form #field_type input').on('change', function() {
showFieldset($(this).val());
});
});
function showFieldset(fieldset_id)
{
$('form fieldset.type_fieldset').hide();
$('form fieldset#type_'+fieldset_id).show();
}
</script>

View File

@ -15,16 +15,14 @@
<colgroup>
<col width="25%">
<col width="30%">
<col width="15%">
<col width="15%">
<col width="30%">
<col width="15%">
</colgroup>
<thead>
<tr>
<th>Actions</th>
<th>Playlist</th>
<th>Weight</th>
<th><abbr title="The likelihood that this playlist will be the source of the next song.">Probability</abbr></th>
<th>Type</th>
<th># Songs</th>
</tr>
</thead>
@ -41,8 +39,25 @@
<br><span class="label label-success">Auto-Assigned</span>
<?php endif; ?>
</td>
<td><?=$row['weight'] ?></td>
<td><?=$row['probability'] ?></td>
<td>
<?php if (!$row['is_enabled']): ?>
Disabled
<?php elseif ($row['type'] == 'default'): ?>
Standard Playlist<br>
Weight: <?=$row['weight'] ?> (<?=$row['probability'] ?>)
<?php elseif ($row['type'] == 'scheduled'): ?>
Scheduled Playlist<br>
Plays between <?=\Entity\StationPlaylist::formatTimeCode($row['schedule_start_time']) ?>
and <?=\Entity\StationPlaylist::formatTimeCode($row['schedule_end_time']) ?>
<?php elseif ($row['type'] == 'once_per_x_songs'): ?>
Once per <?=$row['play_per_songs'] ?> Songs
<?php elseif ($row['type'] == 'once_per_x_minutes'): ?>
Once per <?=$row['play_per_minutes'] ?> Minutes
<?php elseif ($row['type'] == 'once_per_day'): ?>
Once per Day<br>
Plays at <?=\Entity\StationPlaylist::formatTimeCode($row['play_once_time']) ?>
<?php endif; ?>
</td>
<td><?=$row['num_songs'] ?></td>
</tr>
<?php endforeach; ?>

View File

@ -3,22 +3,26 @@ $options = $form->getOptions();
$inner_form = $form->getForm();
?>
<?=$inner_form->openForm() ?>
<?=$inner_form->renderHidden() ?>
<?php foreach($options['groups'] as $fieldset_id => $fieldset): ?>
<?php if (!empty($fieldset['legend'])): ?>
<fieldset id="<?=$fieldset_id ?>">
<fieldset id="<?=$fieldset_id ?>" <?php if (!empty($fieldset['class'])): ?>class="<?=$fieldset['class'] ?>"<?php endif; ?>>
<legend><?=$fieldset['legend'] ?></legend>
<?php if (!empty($fieldset['description'])): ?>
<p><?=$fieldset['description'] ?></p>
<?php endif; ?>
<?php endif; ?>
<?php foreach($fieldset['elements'] as $element_id => $element_info): ?>
<?php if ($element_info[0] == 'submit'): ?>
<input type="submit" value="Save Changes" class="btn btn-lg btn-primary" />
<?php else: ?>
<div class="form-group">
<div class="form-group" id="field_<?=$element_id ?>">
<?=$inner_form->renderLabel($element_id) ?>
<?php if (!empty($element_info[1]['description'])): ?>
<small class="help-block"><?=$element_info[1]['description'] ?></small>