Merge branch 'saas-dev' into saas-tunein

Conflicts:
	airtime_mvc/application/views/scripts/form/preferences.phtml
This commit is contained in:
drigato 2015-05-19 15:50:16 -04:00
commit a2a9e54553
18 changed files with 238 additions and 76 deletions

View file

@ -10,7 +10,7 @@ class ProvisioningHelper
// Parameter values // Parameter values
private $dbuser, $dbpass, $dbname, $dbhost, $dbowner, $apikey; private $dbuser, $dbpass, $dbname, $dbhost, $dbowner, $apikey;
private $instanceId; private $instanceId;
private $station_name, $description; private $stationName, $description;
public function __construct($apikey) public function __construct($apikey)
{ {
@ -40,18 +40,14 @@ class ProvisioningHelper
if ($this->dbhost && !empty($this->dbhost)) { if ($this->dbhost && !empty($this->dbhost)) {
$this->setNewDatabaseConnection(); $this->setNewDatabaseConnection();
//if ($this->checkDatabaseExists()) {
// throw new Exception("ERROR: Airtime database already exists");
//}
if (!$this->checkDatabaseExists()) { if (!$this->checkDatabaseExists()) {
throw new Exception("ERROR: $this->dbname database does not exist."); throw new DatabaseDoesNotExistException("ERROR: $this->dbname database does not exist.");
} }
//We really want to do this check because all the Propel-generated SQL starts with "DROP TABLE IF EXISTS". //We really want to do this check because all the Propel-generated SQL starts with "DROP TABLE IF EXISTS".
//If we don't check, then a second call to this API endpoint would wipe all the tables! //If we don't check, then a second call to this API endpoint would wipe all the tables!
if ($this->checkTablesExist()) { if ($this->checkTablesExist()) {
throw new Exception("ERROR: airtime tables already exists"); throw new DatabaseAlreadyExistsException();
} }
$this->createDatabaseTables(); $this->createDatabaseTables();
@ -63,11 +59,19 @@ class ProvisioningHelper
//All we need to do is create the database tables. //All we need to do is create the database tables.
$this->initializePrefs(); $this->initializePrefs();
} catch (Exception $e) { } catch (DatabaseDoesNotExistException $e) {
http_response_code(400); http_response_code(400);
Logging::error($e->getMessage()); Logging::error($e->getMessage());
echo $e->getMessage() . PHP_EOL; echo $e->getMessage() . PHP_EOL;
return; return;
} catch (DatabaseAlreadyExistsException $e) {
// When we recreate a terminated instance, the process will fail
// if we return a 40x response here. In order to circumvent this,
// just return a 200; we still avoid dropping the existing tables
http_response_code(200);
Logging::info($e->getMessage());
echo $e->getMessage() . PHP_EOL;
return;
} }
http_response_code(201); http_response_code(201);
@ -108,7 +112,7 @@ class ProvisioningHelper
$this->dbowner = $_POST['dbowner']; $this->dbowner = $_POST['dbowner'];
$this->instanceId = $_POST['instanceid']; $this->instanceId = $_POST['instanceid'];
$this->station_name = $_POST['station_name']; $this->stationName = $_POST['station_name'];
$this->description = $_POST['description']; $this->description = $_POST['description'];
} }
@ -194,8 +198,8 @@ class ProvisioningHelper
* Initialize preference values passed from the dashboard (if any exist) * Initialize preference values passed from the dashboard (if any exist)
*/ */
private function initializePrefs() { private function initializePrefs() {
if ($this->station_name) { if ($this->stationName) {
Application_Model_Preference::SetStationName($this->station_name); Application_Model_Preference::SetStationName($this->stationName);
} }
if ($this->description) { if ($this->description) {
Application_Model_Preference::SetStationDescription($this->description); Application_Model_Preference::SetStationDescription($this->description);
@ -203,3 +207,14 @@ class ProvisioningHelper
} }
} }
class DatabaseAlreadyExistsException extends Exception {
private static $_defaultMessage = "ERROR: airtime tables already exists";
public function __construct($message = null, $code = 0, Exception $previous = null) {
$message = _((is_null($message) ? self::$_defaultMessage : $message));
parent::__construct($message, $code, $previous);
}
}
class DatabaseDoesNotExistException extends Exception {}

View file

@ -11,7 +11,7 @@ define('COMPANY_SITE_URL' , 'http://sourcefabric.org/');
define('WHOS_USING_URL' , 'http://sourcefabric.org/en/airtime/whosusing'); define('WHOS_USING_URL' , 'http://sourcefabric.org/en/airtime/whosusing');
define('TERMS_AND_CONDITIONS_URL' , 'http://www.sourcefabric.org/en/about/policy/'); define('TERMS_AND_CONDITIONS_URL' , 'http://www.sourcefabric.org/en/about/policy/');
define('PRIVACY_POLICY_URL' , 'http://www.sourcefabric.org/en/about/policy/'); define('PRIVACY_POLICY_URL' , 'http://www.sourcefabric.org/en/about/policy/');
define('USER_MANUAL_URL' , 'http://sourcefabric.booktype.pro/airtime-25-for-broadcasters/'); define('USER_MANUAL_URL' , 'http://sourcefabric.booktype.pro/airtime-pro-for-broadcasters');
define('LICENSE_VERSION' , 'GNU AGPL v.3'); define('LICENSE_VERSION' , 'GNU AGPL v.3');
define('LICENSE_URL' , 'http://www.gnu.org/licenses/agpl-3.0-standalone.html'); define('LICENSE_URL' , 'http://www.gnu.org/licenses/agpl-3.0-standalone.html');

View file

@ -134,7 +134,7 @@ $pages = array(
), ),
array( array(
'label' => _('User Manual'), 'label' => _('User Manual'),
'uri' => "http://sourcefabric.booktype.pro/airtime-25-for-broadcasters/", 'uri' => "http://sourcefabric.booktype.pro/airtime-pro-for-broadcasters",
'target' => "_blank" 'target' => "_blank"
), ),
array( array(

View file

@ -459,4 +459,69 @@ class PreferenceController extends Zend_Controller_Action
} }
$this->_helper->json->sendJson($out); $this->_helper->json->sendJson($out);
} }
public function deleteAllFilesAction()
{
$this->view->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
// Only admin users should get here through ACL permissioning
// Only allow POST requests
$method = $_SERVER['REQUEST_METHOD'];
if (!($method == 'POST')) {
$this->getResponse()
->setHttpResponseCode(405)
->appendBody(_("Request method not accepted") . ": $method");
return;
}
$user = Application_Model_User::getCurrentUser();
$playlists = $blocks = $streams = [];
$allPlaylists = CcPlaylistQuery::create()->find();
foreach ($allPlaylists as $p) {
$playlists[] = $p->getDbId();
}
$allBlocks = CcBlockQuery::create()->find();
foreach ($allBlocks as $b) {
$blocks[] = $b->getDbId();
}
$allStreams = CcWebstreamQuery::create()->find();
foreach ($allStreams as $s) {
$streams[] = $s->getDbId();
}
// Delete all playlists, blocks, and streams
Application_Model_Playlist::deletePlaylists($playlists, $user->getId());
Application_Model_Block::deleteBlocks($blocks, $user->getId());
Application_Model_Webstream::deleteStreams($streams, $user->getId());
try {
// Delete all the cloud files
$CC_CONFIG = Config::getConfig();
foreach ($CC_CONFIG["supportedStorageBackends"] as $storageBackend) {
$proxyStorageBackend = new ProxyStorageBackend($storageBackend);
$proxyStorageBackend->deleteAllCloudFileObjects();
}
} catch(Exception $e) {
Logging::info($e->getMessage());
}
// Delete all files from the database
$files = CcFilesQuery::create()->find();
foreach ($files as $file) {
$storedFile = new Application_Model_StoredFile($file, null);
// Delete the files quietly to avoid getting Sentry errors for
// every S3 file we delete.
$storedFile->delete(true);
}
$this->getResponse()
->setHttpResponseCode(200)
->appendBody("OK");
}
} }

View file

@ -57,13 +57,16 @@ class ProvisioningController extends Zend_Controller_Action
/** /**
* Delete the Airtime Pro station's files from Amazon S3 * Delete the Airtime Pro station's files from Amazon S3
*
* FIXME: When we deploy this next time, we should ensure that
* this function can only be accessed with POST requests!
*/ */
public function terminateAction() public function terminateAction()
{ {
$this->view->layout()->disableLayout(); $this->view->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true); $this->_helper->viewRenderer->setNoRender(true);
if (!RestAuth::verifyAuth(true, true, $this)) { if (!RestAuth::verifyAuth(true, false, $this)) {
return; return;
} }

View file

@ -0,0 +1,21 @@
<?php
class Application_Form_DangerousPreferences extends Zend_Form_SubForm {
public function init() {
$this->setDecorators(array(
array('ViewScript', array('viewScript' => 'form/preferences_danger.phtml'))
));
$clearLibrary = new Zend_Form_Element_Button('clear_library');
$clearLibrary->setLabel(_('Delete All Tracks in Library'));
//$submit->removeDecorator('Label');
$clearLibrary->setAttribs(array('class'=>'btn centered'));
$clearLibrary->setAttrib('onclick', 'deleteAllFiles();');
$clearLibrary->removeDecorator('DtDdWrapper');
$this->addElement($clearLibrary);
}
}

View file

@ -28,6 +28,9 @@ class Application_Form_Preferences extends Zend_Form
$soundcloud_pref = new Application_Form_SoundcloudPreferences(); $soundcloud_pref = new Application_Form_SoundcloudPreferences();
$this->addSubForm($soundcloud_pref, 'preferences_soundcloud'); $this->addSubForm($soundcloud_pref, 'preferences_soundcloud');
$danger_pref = new Application_Form_DangerousPreferences();
$this->addSubForm($danger_pref, 'preferences_danger');
$submit = new Zend_Form_Element_Submit('submit'); $submit = new Zend_Form_Element_Submit('submit');
$submit->setLabel(_('Save')); $submit->setLabel(_('Save'));
//$submit->removeDecorator('Label'); //$submit->removeDecorator('Label');

View file

@ -375,7 +375,7 @@ SQL;
* Deletes the physical file from the local file system or from the cloud * Deletes the physical file from the local file system or from the cloud
* *
*/ */
public function delete() public function delete($quiet=false)
{ {
// Check if the file is scheduled to be played in the future // Check if the file is scheduled to be played in the future
if (Application_Model_Schedule::IsFileScheduledInTheFuture($this->_file->getCcFileId())) { if (Application_Model_Schedule::IsFileScheduledInTheFuture($this->_file->getCcFileId())) {
@ -405,10 +405,14 @@ SQL;
} }
catch (Exception $e) catch (Exception $e)
{ {
if ($quiet) {
Logging::info($e);
} else {
//Just log the exception and continue. //Just log the exception and continue.
Logging::error($e); Logging::error($e);
} }
} }
}
//Update the user's disk usage //Update the user's disk usage
Application_Model_Preference::updateDiskUsage(-1 * $filesize); Application_Model_Preference::updateDiskUsage(-1 * $filesize);

View file

@ -150,22 +150,26 @@ class Application_Service_CalendarService
$menu["edit"] = array( $menu["edit"] = array(
"name" => _("Edit This Instance"), "name" => _("Edit This Instance"),
"icon" => "edit", "icon" => "edit",
"url" => $baseUrl."Schedule/populate-repeating-show-instance-form"); "url" => $baseUrl . "Schedule/populate-repeating-show-instance-form"
);
} else { } else {
$menu["edit"] = array( $menu["edit"] = array(
"name" => _("Edit"), "name" => _("Edit"),
"icon" => "edit", "icon" => "edit",
"items" => array()); "items" => array()
);
$menu["edit"]["items"]["all"] = array( $menu["edit"]["items"]["all"] = array(
"name" => _("Edit Show"), "name" => _("Edit Show"),
"icon" => "edit", "icon" => "edit",
"url" => $baseUrl."Schedule/populate-show-form"); "url" => $baseUrl . "Schedule/populate-show-form"
);
$menu["edit"]["items"]["instance"] = array( $menu["edit"]["items"]["instance"] = array(
"name" => _("Edit This Instance"), "name" => _("Edit This Instance"),
"icon" => "edit", "icon" => "edit",
"url" => $baseUrl."Schedule/populate-repeating-show-instance-form"); "url" => $baseUrl . "Schedule/populate-repeating-show-instance-form"
);
} }
} else { } else {
$menu["edit"] = array( $menu["edit"] = array(

View file

@ -153,14 +153,19 @@ class Application_Service_ShowFormService
if ($ccShowDay->isShowStartInPast()) { if ($ccShowDay->isShowStartInPast()) {
//for a non-repeating show, we should never allow user to change the start time. //for a non-repeating show, we should never allow user to change the start time.
//for a repeating show, we should allow because the form works as repeating template form //for a repeating show, we should allow because the form works as repeating template form
if (!$ccShowDay->isRepeating()) { $form->disableStartDateAndTime();
// Removing this - if there is no future instance, this will throw an error.
// If there is a future instance, then we get a WHEN block representing the next instance
// which may be confusing.
/*if (!$ccShowDay->isRepeating()) {
$form->disableStartDateAndTime(); $form->disableStartDateAndTime();
} else { } else {
list($showStart, $showEnd) = $this->getNextFutureRepeatShowTime(); list($showStart, $showEnd) = $this->getNextFutureRepeatShowTime();
if ($this->hasShowStarted($showStart)) { if ($this->hasShowStarted($showStart)) {
$form->disableStartDateAndTime(); $form->disableStartDateAndTime();
} }
} }*/
} }
$form->populate( $form->populate(
@ -410,9 +415,8 @@ class Application_Service_ShowFormService
//if the show is repeating, set the start date to the next //if the show is repeating, set the start date to the next
//repeating instance in the future //repeating instance in the future
if ($this->ccShow->isRepeating()) { $originalShowStartDateTime = $this->getCurrentOrNextInstanceStartTime();
list($originalShowStartDateTime,) = $this->getNextFutureRepeatShowTime(); if (!$originalShowStartDateTime) {
} else {
$originalShowStartDateTime = $dt; $originalShowStartDateTime = $dt;
} }
@ -421,26 +425,30 @@ class Application_Service_ShowFormService
/** /**
* *
* Returns 2 DateTime objects, in the user's local time, * Returns a DateTime object, in the user's local time,
* of the next future repeat show instance start and end time * of the current or next show instance start time
*
* Returns null if there is no next future repeating show instance
*/ */
public function getNextFutureRepeatShowTime() public function getCurrentOrNextInstanceStartTime()
{ {
$ccShowInstance = CcShowInstancesQuery::create() $ccShowInstance = CcShowInstancesQuery::create()
->filterByDbShowId($this->ccShow->getDbId()) ->filterByDbShowId($this->ccShow->getDbId())
->filterByDbModifiedInstance(false) ->filterByDbModifiedInstance(false)
->filterByDbStarts(gmdate("Y-m-d H:i:s"), Criteria::GREATER_THAN) ->filterByDbStarts(gmdate("Y-m-d"), Criteria::GREATER_EQUAL)
->orderByDbStarts() ->orderByDbStarts()
->findOne(); ->findOne();
if (!$ccShowInstance) {
return null;
}
$starts = new DateTime($ccShowInstance->getDbStarts(), new DateTimeZone("UTC")); $starts = new DateTime($ccShowInstance->getDbStarts(), new DateTimeZone("UTC"));
$ends = new DateTime($ccShowInstance->getDbEnds(), new DateTimeZone("UTC"));
$showTimezone = $this->ccShow->getFirstCcShowDay()->getDbTimezone(); $showTimezone = $this->ccShow->getFirstCcShowDay()->getDbTimezone();
$starts->setTimezone(new DateTimeZone($showTimezone)); $starts->setTimezone(new DateTimeZone($showTimezone));
$ends->setTimezone(new DateTimeZone($showTimezone));
return array($starts, $ends); return $starts;
} }

View file

@ -252,8 +252,8 @@
success: function(data) { success: function(data) {
if (data.current === null) { if (data.current === null) {
if (data.currentShow != null) { if (data.currentShow != null && data.currentShow.length != 0) {
//Master/show source have no current track but they do have a current show. // Master/show source have no current track but they do have a current show.
$("p.now_playing").html(data.currentShow[0].name); $("p.now_playing").html(data.currentShow[0].name);
} else { } else {
$("p.now_playing").html("Offline"); $("p.now_playing").html("Offline");

View file

@ -2,13 +2,27 @@
<?php echo $this->element->getElement('csrf') ?> <?php echo $this->element->getElement('csrf') ?>
<?php echo $this->element->getSubform('preferences_general') ?> <?php echo $this->element->getSubform('preferences_general') ?>
<?php //No soundcloud stuff on Airtime Pro -- Albert ?>
<h3 class="collapsible-header" id="tunein-pref-heading"><span class="arrow-icon"></span><?php echo _("TuneIn Settings"); ?></h3> <h3 class="collapsible-header" id="tunein-pref-heading"><span class="arrow-icon"></span><?php echo _("TuneIn Settings"); ?></h3>
<div class="collapsible-content" id="tunein-settings"> <div class="collapsible-content" id="tunein-settings">
<?php echo $this->element->getSubform('preferences_tunein') ?> <?php echo $this->element->getSubform('preferences_tunein') ?>
</div> </div>
<?php //No soundcloud stuff on Airtime Pro -- Albert ?>
<h3 class="collapsible-header" id="soundcloud-heading"><span class="arrow-icon"></span><?php echo _("SoundCloud Settings") ?></h3>
<div class="collapsible-content" id="soundcloud-settings">
<?php echo $this->element->getSubform('preferences_soundcloud') ?>
</div>
<h3 class="collapsible-header" id="dangerous-heading"><span class="arrow-icon"></span><?php echo _("Dangerous Options") ?></h3>
<div class="collapsible-content" id="dangerous-settings">
<?php echo $this->element->getSubform('preferences_danger') ?>
</div>
<br>
<?php echo $this->element->submit->render() ?> <?php echo $this->element->submit->render() ?>
</form> </form>

View file

@ -0,0 +1,14 @@
<fieldset class="padded">
<dl class="zend_form">
<div class="warning" style="margin-bottom: 10px;">
<p class="warning-label">
<strong>Warning:</strong> These functions will have <strong>permanent and lasting effects</strong>
on your Airtime station. Think carefully before using them!
</p>
</div>
<?php echo $this->element->getElement('clear_library')->render() ?>
</dl>
</fieldset>

View file

@ -3,12 +3,11 @@
font-size: 200px !important; font-size: 200px !important;
} }
</style> </style>
<?php $upgradeLink = Application_Common_OsPath::getBaseDir() . "billing/upgrade"; ?>
<?php if ($this->quotaLimitReached) { ?> <?php if ($this->quotaLimitReached) { ?>
<div class="errors quota-reached"> <div class="errors quota-reached">
Disk quota exceeded. You cannot upload files until you <?php printf(_pro("Disk quota exceeded. You cannot upload files until you %s upgrade your storage"),
<a target="_parent" href=<?php $baseUrl = Application_Common_OsPath::getBaseDir(); echo $baseUrl . "billing/upgrade"?>> "<a target=\"_parent\" href=$upgradeLink>");?></a>.
upgrade your storage
</a>.
</div> </div>
<?php <?php
} }

View file

@ -1788,7 +1788,7 @@ ul.errors {
width:278px; width:278px;
} }
ul.errors li { ul.errors li, .warning {
color:#902d2d; color:#902d2d;
font-size:11px; font-size:11px;
padding:2px 4px; padding:2px 4px;
@ -1798,6 +1798,11 @@ ul.errors li {
list-style: none; list-style: none;
} }
.warning-label {
font-size: medium;
text-align: center;
}
div.success{ div.success{
color:#3B5323; color:#3B5323;
font-size:11px; font-size:11px;
@ -2255,14 +2260,9 @@ dd.radio-inline-list, .preferences dd.radio-inline-list, .stream-config dd.radio
.radio-inline-list label { .radio-inline-list label {
margin-right:12px; margin-right:12px;
} }
.preferences.simple-formblock dd.block-display { .preferences.simple-formblock dd.block-display,
width: 100%; .preferences.simple-formblock dd.block-display select, .stream-config.simple-formblock dd.block-display select,
} .preferences dd.block-display .input_select, .stream-config dd.block-display .input_select {
.preferences.simple-formblock dd.block-display select, .stream-config.simple-formblock dd.block-display select {
width: 100%;
}
.preferences dd.block-display .input_select, .stream-config dd.block-display .input_select {
width: 100%; width: 100%;
} }
.preferences dd.block-display .input_text_area, .preferences dd.block-display .input_text .preferences dd.block-display .input_text_area, .preferences dd.block-display .input_text
@ -2284,6 +2284,15 @@ dd.radio-inline-list, .preferences dd.radio-inline-list, .stream-config dd.radio
margin-bottom: 4px; margin-bottom: 4px;
} }
.preferences #Logo-img-container {
margin-top: 30px;
}
.centered {
margin: 0 auto;
display: block;
}
#show_time_info { #show_time_info {
font-size:12px; font-size:12px;
height:30px; height:30px;

View file

@ -1,18 +1,13 @@
function showErrorSections() { function showErrorSections() {
if($("#soundcloud-settings .errors").length > 0) { var selector = $("[id$=-settings]");
$("#soundcloud-settings").show(); selector.each(function(i) {
$(window).scrollTop($("#soundcloud-settings .errors").position().top); var el = $(this);
} var errors = el.find(".errors");
if (errors.length > 0) {
if($("#email-server-settings .errors").length > 0) { el.show();
$("#email-server-settings").show(); $(window).scrollTop(errors.position().top);
$(window).scrollTop($("#email-server-settings .errors").position().top);
}
if($("#livestream-settings .errors").length > 0) {
$("#livestream-settings").show();
$(window).scrollTop($("#livestream-settings .errors").position().top);
} }
});
} }
function setConfigureMailServerListener() { function setConfigureMailServerListener() {
@ -144,6 +139,14 @@ function removeLogo() {
location.reload(); location.reload();
} }
function deleteAllFiles() {
var resp = confirm($.i18n._("Are you sure you want to delete all the tracks in your library?"))
if (resp) {
$.post(baseUrl+'Preference/delete-all-files', function(json){});
location.reload();
}
}
$(document).ready(function() { $(document).ready(function() {
$('.collapsible-header').live('click',function() { $('.collapsible-header').live('click',function() {

View file

@ -3,12 +3,8 @@ import json
import logging import logging
import collections import collections
import Queue import Queue
import subprocess
import multiprocessing
import time import time
import sys
import traceback import traceback
import os
import pickle import pickle
import threading import threading
from urlparse import urlparse from urlparse import urlparse
@ -158,23 +154,23 @@ class StatusReporter():
''' We use multiprocessing.Process again here because we need a thread for this stuff ''' We use multiprocessing.Process again here because we need a thread for this stuff
anyways, and Python gives us process isolation for free (crash safety). anyways, and Python gives us process isolation for free (crash safety).
''' '''
_ipc_queue = multiprocessing.Queue() _ipc_queue = Queue.Queue()
#_request_process = multiprocessing.Process(target=process_http_requests, #_http_thread = multiprocessing.Process(target=process_http_requests,
# args=(_ipc_queue,)) # args=(_ipc_queue,))
_request_process = None _http_thread = None
@classmethod @classmethod
def start_thread(self, http_retry_queue_path): def start_thread(self, http_retry_queue_path):
StatusReporter._request_process = threading.Thread(target=process_http_requests, StatusReporter._http_thread = threading.Thread(target=process_http_requests,
args=(StatusReporter._ipc_queue,http_retry_queue_path)) args=(StatusReporter._ipc_queue,http_retry_queue_path))
StatusReporter._request_process.start() StatusReporter._http_thread.start()
@classmethod @classmethod
def stop_thread(self): def stop_thread(self):
logging.info("Terminating status_reporter process") logging.info("Terminating status_reporter process")
#StatusReporter._request_process.terminate() # Triggers SIGTERM on the child process #StatusReporter._http_thread.terminate() # Triggers SIGTERM on the child process
StatusReporter._ipc_queue.put("shutdown") # Special trigger StatusReporter._ipc_queue.put("shutdown") # Special trigger
StatusReporter._request_process.join() StatusReporter._http_thread.join()
@classmethod @classmethod
def _send_http_request(self, request): def _send_http_request(self, request):

View file

@ -249,6 +249,7 @@ s = if dj_live_stream_port != 0 and dj_live_stream_mp != "" then
on_connect=live_dj_connect, on_connect=live_dj_connect,
on_disconnect=live_dj_disconnect)) on_disconnect=live_dj_disconnect))
dj_live = on_metadata(notify_queue, dj_live)
ignore(output.dummy(dj_live, fallible=true)) ignore(output.dummy(dj_live, fallible=true))
switch(id="show_schedule_noise_switch", switch(id="show_schedule_noise_switch",
@ -271,6 +272,7 @@ s = if master_live_stream_port != 0 and master_live_stream_mp != "" then
on_connect=master_dj_connect, on_connect=master_dj_connect,
on_disconnect=master_dj_disconnect)) on_disconnect=master_dj_disconnect))
master_dj = on_metadata(notify_queue, master_dj)
ignore(output.dummy(master_dj, fallible=true)) ignore(output.dummy(master_dj, fallible=true))
switch(id="master_show_schedule_noise_switch", switch(id="master_show_schedule_noise_switch",
@ -282,6 +284,8 @@ else
s s
end end
# Send metadata notifications when using master source
s = on_metadata(notify_queue, s)
# Attach a skip command to the source s: # Attach a skip command to the source s:
#add_skip_command(s) #add_skip_command(s)