diff --git a/app/library/App/Radio/Automation.php b/app/library/App/Radio/Automation.php index 3ed16a212..7fee04692 100644 --- a/app/library/App/Radio/Automation.php +++ b/app/library/App/Radio/Automation.php @@ -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) diff --git a/app/library/App/Radio/Backend/LiquidSoap.php b/app/library/App/Radio/Backend/LiquidSoap.php index 81961f2f8..aee3e4786 100644 --- a/app/library/App/Radio/Backend/LiquidSoap.php +++ b/app/library/App/Radio/Backend/LiquidSoap.php @@ -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(); diff --git a/app/library/App/Utilities.php b/app/library/App/Utilities.php index dab0c8212..a9a278730 100644 --- a/app/library/App/Utilities.php +++ b/app/library/App/Utilities.php @@ -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'; + } } diff --git a/app/models/Entity/Station.php b/app/models/Entity/Station.php index 20b27e34d..d7ca1234d 100644 --- a/app/models/Entity/Station.php +++ b/app/models/Entity/Station.php @@ -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; diff --git a/app/models/Entity/StationPlaylist.php b/app/models/Entity/StationPlaylist.php index 8e50b932e..bcb2b293c 100644 --- a/app/models/Entity/StationPlaylist.php +++ b/app/models/Entity/StationPlaylist.php @@ -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; + } } \ No newline at end of file diff --git a/app/modules/stations/config/forms/playlist.conf.php b/app/modules/stations/config/forms/playlist.conf.php index c4c002a47..072558b3b 100644 --- a/app/modules/stations/config/forms/playlist.conf.php +++ b/app/modules/stations/config/forms/playlist.conf.php @@ -1,47 +1,179 @@ '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' => 'Standard: Plays all day, shuffles with other standard playlists based on weight.', + 'scheduled' => 'Scheduled: Play during a scheduled time range. Useful for mood-based time playlists.', + 'once_per_x_songs' => 'Once per x Songs: Play exactly once every x songs. Useful for station ID/jingles.', + 'once_per_x_minutes' => 'Once Per x Minutes: Play exactly once every x minutes. Useful for station ID/jingles.', + 'once_per_day' => 'Daily: 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 '.date('g:ia').'.', + '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 '.date('g:ia').'.', + 'options' => $hour_select, + ]], + + ], + ], + + 'grp_submit' => [ + 'elements' => [ + + 'submit' => ['submit', [ + 'type' => 'submit', + 'label' => 'Save Changes', + 'helper' => 'formButton', + 'class' => 'ui-button btn-lg btn-primary', + ]], + + ], + ], ], ]; \ No newline at end of file diff --git a/app/modules/stations/controllers/PlaylistsController.php b/app/modules/stations/controllers/PlaylistsController.php index 230b6348c..4f87726d3 100644 --- a/app/modules/stations/controllers/PlaylistsController.php +++ b/app/modules/stations/controllers/PlaylistsController.php @@ -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('Stream updated!', '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() diff --git a/app/modules/stations/views/scripts/playlists/edit.phtml b/app/modules/stations/views/scripts/playlists/edit.phtml new file mode 100644 index 000000000..23bd18f21 --- /dev/null +++ b/app/modules/stations/views/scripts/playlists/edit.phtml @@ -0,0 +1,31 @@ +layout('main', ['manual' => true]) ?> + +
+

name ?>

+
+ +
+
+

+ + insert('system/form') ?> +
+
+ + \ No newline at end of file diff --git a/app/modules/stations/views/scripts/playlists/index.phtml b/app/modules/stations/views/scripts/playlists/index.phtml index 334e0b768..a5be4f8ce 100644 --- a/app/modules/stations/views/scripts/playlists/index.phtml +++ b/app/modules/stations/views/scripts/playlists/index.phtml @@ -15,16 +15,14 @@ - - + Actions Playlist - Weight - Probability + Type # Songs @@ -41,8 +39,25 @@
Auto-Assigned - - + + + Disabled + + Standard Playlist
+ Weight: () + + Scheduled Playlist
+ Plays between + and + + Once per Songs + + Once per Minutes + + Once per Day
+ Plays at + + diff --git a/app/templates/system/form.phtml b/app/templates/system/form.phtml index 3b99752a7..31cf084c4 100644 --- a/app/templates/system/form.phtml +++ b/app/templates/system/form.phtml @@ -3,22 +3,26 @@ $options = $form->getOptions(); $inner_form = $form->getForm(); ?> - openForm() ?> renderHidden() ?> $fieldset): ?> -
+
class=""> + + +

+ + $element_info): ?> -
+
renderLabel($element_id) ?>