diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php index 580c20e52..605617105 100644 --- a/airtime_mvc/application/common/ProvisioningHelper.php +++ b/airtime_mvc/application/common/ProvisioningHelper.php @@ -10,7 +10,7 @@ class ProvisioningHelper // Parameter values private $dbuser, $dbpass, $dbname, $dbhost, $dbowner, $apikey; private $instanceId; - private $station_name, $description; + private $stationName, $description; public function __construct($apikey) { @@ -40,18 +40,14 @@ class ProvisioningHelper if ($this->dbhost && !empty($this->dbhost)) { $this->setNewDatabaseConnection(); - //if ($this->checkDatabaseExists()) { - // throw new Exception("ERROR: Airtime database already exists"); - //} - 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". //If we don't check, then a second call to this API endpoint would wipe all the tables! if ($this->checkTablesExist()) { - throw new Exception("ERROR: airtime tables already exists"); + throw new DatabaseAlreadyExistsException(); } $this->createDatabaseTables(); @@ -63,11 +59,19 @@ class ProvisioningHelper //All we need to do is create the database tables. $this->initializePrefs(); - } catch (Exception $e) { + } catch (DatabaseDoesNotExistException $e) { http_response_code(400); Logging::error($e->getMessage()); echo $e->getMessage() . PHP_EOL; 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); @@ -108,7 +112,7 @@ class ProvisioningHelper $this->dbowner = $_POST['dbowner']; $this->instanceId = $_POST['instanceid']; - $this->station_name = $_POST['station_name']; + $this->stationName = $_POST['station_name']; $this->description = $_POST['description']; } @@ -194,8 +198,8 @@ class ProvisioningHelper * Initialize preference values passed from the dashboard (if any exist) */ private function initializePrefs() { - if ($this->station_name) { - Application_Model_Preference::SetStationName($this->station_name); + if ($this->stationName) { + Application_Model_Preference::SetStationName($this->stationName); } if ($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 {} + diff --git a/airtime_mvc/application/configs/constants.php b/airtime_mvc/application/configs/constants.php index bb1f6703a..bb5184895 100644 --- a/airtime_mvc/application/configs/constants.php +++ b/airtime_mvc/application/configs/constants.php @@ -11,7 +11,7 @@ define('COMPANY_SITE_URL' , 'http://sourcefabric.org/'); define('WHOS_USING_URL' , 'http://sourcefabric.org/en/airtime/whosusing'); define('TERMS_AND_CONDITIONS_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_URL' , 'http://www.gnu.org/licenses/agpl-3.0-standalone.html'); diff --git a/airtime_mvc/application/configs/navigation.php b/airtime_mvc/application/configs/navigation.php index 1aeab45d5..47b32b714 100644 --- a/airtime_mvc/application/configs/navigation.php +++ b/airtime_mvc/application/configs/navigation.php @@ -134,7 +134,7 @@ $pages = array( ), array( 'label' => _('User Manual'), - 'uri' => "http://sourcefabric.booktype.pro/airtime-25-for-broadcasters/", + 'uri' => "http://sourcefabric.booktype.pro/airtime-pro-for-broadcasters", 'target' => "_blank" ), array( diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php index e4615c02d..1a67b556d 100644 --- a/airtime_mvc/application/controllers/PreferenceController.php +++ b/airtime_mvc/application/controllers/PreferenceController.php @@ -459,4 +459,69 @@ class PreferenceController extends Zend_Controller_Action } $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"); + } + } diff --git a/airtime_mvc/application/controllers/ProvisioningController.php b/airtime_mvc/application/controllers/ProvisioningController.php index cda59e0db..06ee7a2ab 100644 --- a/airtime_mvc/application/controllers/ProvisioningController.php +++ b/airtime_mvc/application/controllers/ProvisioningController.php @@ -57,13 +57,16 @@ class ProvisioningController extends Zend_Controller_Action /** * 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() { $this->view->layout()->disableLayout(); $this->_helper->viewRenderer->setNoRender(true); - if (!RestAuth::verifyAuth(true, true, $this)) { + if (!RestAuth::verifyAuth(true, false, $this)) { return; } diff --git a/airtime_mvc/application/forms/DangerousPreferences.php b/airtime_mvc/application/forms/DangerousPreferences.php new file mode 100644 index 000000000..28d203405 --- /dev/null +++ b/airtime_mvc/application/forms/DangerousPreferences.php @@ -0,0 +1,21 @@ +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); + } + +} diff --git a/airtime_mvc/application/forms/Preferences.php b/airtime_mvc/application/forms/Preferences.php index 3226d558d..7973e6ddb 100644 --- a/airtime_mvc/application/forms/Preferences.php +++ b/airtime_mvc/application/forms/Preferences.php @@ -28,6 +28,9 @@ class Application_Form_Preferences extends Zend_Form $soundcloud_pref = new Application_Form_SoundcloudPreferences(); $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->setLabel(_('Save')); //$submit->removeDecorator('Label'); diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index 8e3dd7a59..32d6ec371 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -375,7 +375,7 @@ SQL; * 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 if (Application_Model_Schedule::IsFileScheduledInTheFuture($this->_file->getCcFileId())) { @@ -405,8 +405,12 @@ SQL; } catch (Exception $e) { - //Just log the exception and continue. - Logging::error($e); + if ($quiet) { + Logging::info($e); + } else { + //Just log the exception and continue. + Logging::error($e); + } } } diff --git a/airtime_mvc/application/services/CalendarService.php b/airtime_mvc/application/services/CalendarService.php index c90adf3a5..ed879e90d 100644 --- a/airtime_mvc/application/services/CalendarService.php +++ b/airtime_mvc/application/services/CalendarService.php @@ -150,23 +150,27 @@ class Application_Service_CalendarService $menu["edit"] = array( "name" => _("Edit This Instance"), "icon" => "edit", - "url" => $baseUrl."Schedule/populate-repeating-show-instance-form"); + "url" => $baseUrl . "Schedule/populate-repeating-show-instance-form" + ); } else { $menu["edit"] = array( "name" => _("Edit"), "icon" => "edit", - "items" => array()); + "items" => array() + ); $menu["edit"]["items"]["all"] = array( "name" => _("Edit Show"), "icon" => "edit", - "url" => $baseUrl."Schedule/populate-show-form"); + "url" => $baseUrl . "Schedule/populate-show-form" + ); $menu["edit"]["items"]["instance"] = array( "name" => _("Edit This Instance"), "icon" => "edit", - "url" => $baseUrl."Schedule/populate-repeating-show-instance-form"); - } + "url" => $baseUrl . "Schedule/populate-repeating-show-instance-form" + ); + } } else { $menu["edit"] = array( "name"=> _("Edit Show"), diff --git a/airtime_mvc/application/services/ShowFormService.php b/airtime_mvc/application/services/ShowFormService.php index fb58e7e6f..ed610757e 100644 --- a/airtime_mvc/application/services/ShowFormService.php +++ b/airtime_mvc/application/services/ShowFormService.php @@ -153,14 +153,19 @@ class Application_Service_ShowFormService if ($ccShowDay->isShowStartInPast()) { //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 - 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(); } else { list($showStart, $showEnd) = $this->getNextFutureRepeatShowTime(); if ($this->hasShowStarted($showStart)) { $form->disableStartDateAndTime(); } - } + }*/ } $form->populate( @@ -410,9 +415,8 @@ class Application_Service_ShowFormService //if the show is repeating, set the start date to the next //repeating instance in the future - if ($this->ccShow->isRepeating()) { - list($originalShowStartDateTime,) = $this->getNextFutureRepeatShowTime(); - } else { + $originalShowStartDateTime = $this->getCurrentOrNextInstanceStartTime(); + if (!$originalShowStartDateTime) { $originalShowStartDateTime = $dt; } @@ -421,26 +425,30 @@ class Application_Service_ShowFormService /** * - * Returns 2 DateTime objects, in the user's local time, - * of the next future repeat show instance start and end time + * Returns a DateTime object, in the user's local 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() ->filterByDbShowId($this->ccShow->getDbId()) ->filterByDbModifiedInstance(false) - ->filterByDbStarts(gmdate("Y-m-d H:i:s"), Criteria::GREATER_THAN) + ->filterByDbStarts(gmdate("Y-m-d"), Criteria::GREATER_EQUAL) ->orderByDbStarts() ->findOne(); + + if (!$ccShowInstance) { + return null; + } $starts = new DateTime($ccShowInstance->getDbStarts(), new DateTimeZone("UTC")); - $ends = new DateTime($ccShowInstance->getDbEnds(), new DateTimeZone("UTC")); $showTimezone = $this->ccShow->getFirstCcShowDay()->getDbTimezone(); $starts->setTimezone(new DateTimeZone($showTimezone)); - $ends->setTimezone(new DateTimeZone($showTimezone)); - return array($starts, $ends); + return $starts; } diff --git a/airtime_mvc/application/views/scripts/embed/player.phtml b/airtime_mvc/application/views/scripts/embed/player.phtml index 30d4736bc..6c3b1d06b 100644 --- a/airtime_mvc/application/views/scripts/embed/player.phtml +++ b/airtime_mvc/application/views/scripts/embed/player.phtml @@ -252,8 +252,8 @@ success: function(data) { if (data.current === null) { - if (data.currentShow != null) { - //Master/show source have no current track but they do have a current show. + if (data.currentShow != null && data.currentShow.length != 0) { + // Master/show source have no current track but they do have a current show. $("p.now_playing").html(data.currentShow[0].name); } else { $("p.now_playing").html("Offline"); diff --git a/airtime_mvc/application/views/scripts/form/preferences.phtml b/airtime_mvc/application/views/scripts/form/preferences.phtml index 9029a5c17..f7e4db149 100644 --- a/airtime_mvc/application/views/scripts/form/preferences.phtml +++ b/airtime_mvc/application/views/scripts/form/preferences.phtml @@ -2,13 +2,27 @@ element->getElement('csrf') ?> element->getSubform('preferences_general') ?> -

element->getSubform('preferences_tunein') ?>
+ + +

+ +
+ element->getSubform('preferences_soundcloud') ?> +
+ +

+
+ element->getSubform('preferences_danger') ?> +
+ +
+ element->submit->render() ?> diff --git a/airtime_mvc/application/views/scripts/form/preferences_danger.phtml b/airtime_mvc/application/views/scripts/form/preferences_danger.phtml new file mode 100644 index 000000000..7ea8a78d1 --- /dev/null +++ b/airtime_mvc/application/views/scripts/form/preferences_danger.phtml @@ -0,0 +1,14 @@ +
+
+ +
+

+ Warning: These functions will have permanent and lasting effects + on your Airtime station. Think carefully before using them! +

+
+ + element->getElement('clear_library')->render() ?> + +
+
diff --git a/airtime_mvc/application/views/scripts/plupload/index.phtml b/airtime_mvc/application/views/scripts/plupload/index.phtml index 00d8caf96..6424b740e 100644 --- a/airtime_mvc/application/views/scripts/plupload/index.phtml +++ b/airtime_mvc/application/views/scripts/plupload/index.phtml @@ -3,12 +3,11 @@ font-size: 200px !important; } + quotaLimitReached) { ?>
- Disk quota exceeded. You cannot upload files until you - > - upgrade your storage - . + ");?>.
0) { - $("#soundcloud-settings").show(); - $(window).scrollTop($("#soundcloud-settings .errors").position().top); - } - - if($("#email-server-settings .errors").length > 0) { - $("#email-server-settings").show(); - $(window).scrollTop($("#email-server-settings .errors").position().top); - } - - if($("#livestream-settings .errors").length > 0) { - $("#livestream-settings").show(); - $(window).scrollTop($("#livestream-settings .errors").position().top); - } + var selector = $("[id$=-settings]"); + selector.each(function(i) { + var el = $(this); + var errors = el.find(".errors"); + if (errors.length > 0) { + el.show(); + $(window).scrollTop(errors.position().top); + } + }); } function setConfigureMailServerListener() { @@ -144,6 +139,14 @@ function removeLogo() { 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() { $('.collapsible-header').live('click',function() { diff --git a/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py b/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py index bd1730b3b..63c859d05 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py @@ -3,12 +3,8 @@ import json import logging import collections import Queue -import subprocess -import multiprocessing import time -import sys import traceback -import os import pickle import threading from urlparse import urlparse @@ -158,23 +154,23 @@ class StatusReporter(): ''' 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). ''' - _ipc_queue = multiprocessing.Queue() - #_request_process = multiprocessing.Process(target=process_http_requests, + _ipc_queue = Queue.Queue() + #_http_thread = multiprocessing.Process(target=process_http_requests, # args=(_ipc_queue,)) - _request_process = None + _http_thread = None @classmethod 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)) - StatusReporter._request_process.start() + StatusReporter._http_thread.start() @classmethod def stop_thread(self): 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._request_process.join() + StatusReporter._http_thread.join() @classmethod def _send_http_request(self, request): diff --git a/python_apps/pypo/liquidsoap_scripts/ls_script.liq b/python_apps/pypo/liquidsoap_scripts/ls_script.liq index ccc3a0776..fb3452181 100644 --- a/python_apps/pypo/liquidsoap_scripts/ls_script.liq +++ b/python_apps/pypo/liquidsoap_scripts/ls_script.liq @@ -249,6 +249,7 @@ s = if dj_live_stream_port != 0 and dj_live_stream_mp != "" then on_connect=live_dj_connect, on_disconnect=live_dj_disconnect)) + dj_live = on_metadata(notify_queue, dj_live) ignore(output.dummy(dj_live, fallible=true)) 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_disconnect=master_dj_disconnect)) + master_dj = on_metadata(notify_queue, master_dj) ignore(output.dummy(master_dj, fallible=true)) switch(id="master_show_schedule_noise_switch", @@ -282,6 +284,8 @@ else s end +# Send metadata notifications when using master source +s = on_metadata(notify_queue, s) # Attach a skip command to the source s: #add_skip_command(s)